Files
MorphDoc/packages/markdown-echarts/src/parse-yaml.ts
T
SkyJourney f5a46f79f1 feat: 增加 ECharts 渲染与预览交互增强
- 新增可复用的 markdown-echarts 工作区,支持安全的 YAML 围栏、默认值、语义校验、错误占位和 SVG 渲染
- 将 ECharts 接入统一 Markdown、快速预览、精确预览与 PDF 链路,并复用 Mermaid 的不可跨页和超高图单页适配策略
- 默认示例加入 ECharts,增加源文本折叠、顶部文档状态、页码输入跳转与滚动同步
- 快速预览改用外层容器滚动,使滚动条与精确预览统一贴在预览区域右侧,并兼容显示缩放
- 完善 Docker 工作区复制、可配置 Compose 镜像、测试覆盖和进度文档
2026-07-27 16:05:14 +08:00

161 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
isAlias,
isMap,
isScalar,
isSeq,
parseDocument
} from "yaml";
import type { Pair, ParsedNode } from "yaml";
import {
DEFAULT_ECHARTS_FENCE_HEIGHT,
EChartsFenceError,
echartsFenceSpecSchema,
type NormalizedEChartsFenceSpec
} from "./protocol.js";
import {
defaultEChartsSecurityLimits,
normalizeAndValidateEChartsOption,
type EChartsSecurityLimits
} from "./security-policy.js";
import {
validateAndApplyEChartsOptionDefaults
} from "./semantic-validator.js";
export interface ParseEChartsFenceOptions {
maxSourceBytes?: number;
securityLimits?: EChartsSecurityLimits;
}
export const defaultMaxEChartsFenceSourceBytes = 512 * 1024;
function inspectYamlNode(node: ParsedNode | null | undefined): void {
if (!node) {
return;
}
if (isAlias(node)) {
throw new EChartsFenceError(
"YAML_ALIAS_NOT_ALLOWED",
"ECharts YAML 不允许使用别名"
);
}
if (node.anchor !== undefined) {
throw new EChartsFenceError(
"YAML_ANCHOR_NOT_ALLOWED",
"ECharts YAML 不允许使用锚点"
);
}
if (node.tag !== undefined) {
throw new EChartsFenceError(
"YAML_TAG_NOT_ALLOWED",
"ECharts YAML 不允许使用显式标签"
);
}
if (isMap(node)) {
for (const pair of node.items as Pair<ParsedNode, ParsedNode>[]) {
inspectYamlNode(pair.key);
inspectYamlNode(pair.value);
}
return;
}
if (isSeq(node)) {
for (const item of node.items) {
inspectYamlNode(item);
}
return;
}
if (!isScalar(node)) {
throw new EChartsFenceError(
"INVALID_YAML",
"ECharts YAML 包含无法识别的节点"
);
}
}
function formatZodError(error: {
issues: { path: PropertyKey[]; message: string }[];
}) {
return error.issues
.map((issue) => {
const path = issue.path.join(".");
return path ? `${path}${issue.message}` : issue.message;
})
.join("");
}
export function parseEChartsFence(
source: string,
options: ParseEChartsFenceOptions = {}
): NormalizedEChartsFenceSpec {
const maxSourceBytes =
options.maxSourceBytes ?? defaultMaxEChartsFenceSourceBytes;
if (new TextEncoder().encode(source).byteLength > maxSourceBytes) {
throw new EChartsFenceError(
"SOURCE_TOO_LARGE",
`ECharts YAML 不能超过 ${maxSourceBytes} 字节`
);
}
const document = parseDocument(source, {
schema: "core",
strict: true,
uniqueKeys: true,
prettyErrors: true
});
if (document.errors.length > 0) {
throw new EChartsFenceError(
"INVALID_YAML",
document.errors.map((error) => error.message).join("")
);
}
inspectYamlNode(document.contents);
let parsed: unknown;
try {
parsed = document.toJS({
maxAliasCount: 0,
mapAsMap: false
});
} catch (error) {
throw new EChartsFenceError(
"INVALID_YAML",
error instanceof Error ? error.message : "ECharts YAML 解析失败",
{ cause: error }
);
}
const protocolResult = echartsFenceSpecSchema.safeParse(parsed);
if (!protocolResult.success) {
throw new EChartsFenceError(
"INVALID_PROTOCOL",
formatZodError(protocolResult.error)
);
}
const securityLimits =
options.securityLimits ?? defaultEChartsSecurityLimits;
const option = validateAndApplyEChartsOptionDefaults(
normalizeAndValidateEChartsOption(
protocolResult.data.option,
securityLimits
)
);
const normalized: NormalizedEChartsFenceSpec = {
version: protocolResult.data.version,
theme: protocolResult.data.theme,
option
};
if (protocolResult.data.height !== undefined) {
normalized.height = protocolResult.data.height;
} else if (protocolResult.data.aspectRatio === undefined) {
normalized.height = DEFAULT_ECHARTS_FENCE_HEIGHT;
}
if (protocolResult.data.aspectRatio !== undefined) {
normalized.aspectRatio = protocolResult.data.aspectRatio;
}
if (protocolResult.data.caption !== undefined) {
normalized.caption = protocolResult.data.caption;
}
return normalized;
}