feat: 增加 ECharts 渲染与预览交互增强
- 新增可复用的 markdown-echarts 工作区,支持安全的 YAML 围栏、默认值、语义校验、错误占位和 SVG 渲染 - 将 ECharts 接入统一 Markdown、快速预览、精确预览与 PDF 链路,并复用 Mermaid 的不可跨页和超高图单页适配策略 - 默认示例加入 ECharts,增加源文本折叠、顶部文档状态、页码输入跳转与滚动同步 - 快速预览改用外层容器滚动,使滚动条与精确预览统一贴在预览区域右侧,并兼容显示缩放 - 完善 Docker 工作区复制、可配置 Compose 镜像、测试覆盖和进度文档
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
import {
|
||||
getPaperDimensionsMm,
|
||||
lengthToMillimeters,
|
||||
millimetersToCssPixels,
|
||||
type ExportConfig
|
||||
} from "@md-to-pdf/core";
|
||||
|
||||
export interface DiagramDimensions {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface DiagramPageFit extends DiagramDimensions {
|
||||
scaled: boolean;
|
||||
}
|
||||
|
||||
export interface SvgDiagramFitOptions {
|
||||
containerSelector: string;
|
||||
svgSelector?: string;
|
||||
resolvePageContent?: (
|
||||
container: HTMLElement,
|
||||
pageContent: DiagramDimensions
|
||||
) => DiagramDimensions;
|
||||
onFit?: (
|
||||
container: HTMLElement,
|
||||
svg: SVGSVGElement,
|
||||
fit: DiagramPageFit
|
||||
) => void;
|
||||
}
|
||||
|
||||
const PAGE_FIT_EPSILON_PX = 1;
|
||||
|
||||
export function parseSvgViewBox(
|
||||
value: string | null
|
||||
): DiagramDimensions | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const values = value
|
||||
.trim()
|
||||
.split(/[\s,]+/)
|
||||
.map((part) => Number(part));
|
||||
const [, , width = Number.NaN, height = Number.NaN] = values;
|
||||
|
||||
if (
|
||||
values.length !== 4 ||
|
||||
!Number.isFinite(width) ||
|
||||
!Number.isFinite(height) ||
|
||||
width <= 0 ||
|
||||
height <= 0
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
function parseSvgDimensions(
|
||||
svg: SVGSVGElement
|
||||
): DiagramDimensions | undefined {
|
||||
const width = Number.parseFloat(svg.getAttribute("width") ?? "");
|
||||
const height = Number.parseFloat(
|
||||
svg.getAttribute("height") ?? ""
|
||||
);
|
||||
if (
|
||||
!Number.isFinite(width) ||
|
||||
!Number.isFinite(height) ||
|
||||
width <= 0 ||
|
||||
height <= 0
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
export function getPageContentDimensions(
|
||||
config: ExportConfig
|
||||
): DiagramDimensions {
|
||||
const paper = getPaperDimensionsMm(
|
||||
config.paper.format,
|
||||
config.paper.orientation
|
||||
);
|
||||
const width =
|
||||
paper.width -
|
||||
lengthToMillimeters(config.paper.margins.left) -
|
||||
lengthToMillimeters(config.paper.margins.right);
|
||||
const height =
|
||||
paper.height -
|
||||
lengthToMillimeters(config.paper.margins.top) -
|
||||
lengthToMillimeters(config.paper.margins.bottom);
|
||||
|
||||
return {
|
||||
width: millimetersToCssPixels(width),
|
||||
height: millimetersToCssPixels(height)
|
||||
};
|
||||
}
|
||||
|
||||
export function calculateDiagramPageFit(
|
||||
intrinsic: DiagramDimensions,
|
||||
pageContent: DiagramDimensions
|
||||
): DiagramPageFit {
|
||||
const heightAfterWidthFit =
|
||||
(pageContent.width * intrinsic.height) / intrinsic.width;
|
||||
|
||||
if (heightAfterWidthFit <= pageContent.height) {
|
||||
return {
|
||||
width: pageContent.width,
|
||||
height: heightAfterWidthFit,
|
||||
scaled: false
|
||||
};
|
||||
}
|
||||
|
||||
const height = Math.max(
|
||||
0,
|
||||
pageContent.height - PAGE_FIT_EPSILON_PX
|
||||
);
|
||||
return {
|
||||
width: (height * intrinsic.width) / intrinsic.height,
|
||||
height,
|
||||
scaled: true
|
||||
};
|
||||
}
|
||||
|
||||
export function fitSvgDiagramsToPage(
|
||||
root: ParentNode,
|
||||
config: ExportConfig,
|
||||
options: SvgDiagramFitOptions
|
||||
) {
|
||||
const pageContent = getPageContentDimensions(config);
|
||||
const containers = Array.from(
|
||||
root.querySelectorAll<HTMLElement>(options.containerSelector)
|
||||
);
|
||||
|
||||
for (const container of containers) {
|
||||
const svg = container.querySelector<SVGSVGElement>(
|
||||
options.svgSelector ?? "svg"
|
||||
);
|
||||
const intrinsic = svg
|
||||
? parseSvgViewBox(svg.getAttribute("viewBox")) ??
|
||||
parseSvgDimensions(svg)
|
||||
: undefined;
|
||||
if (!svg || !intrinsic) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const availablePageContent =
|
||||
options.resolvePageContent?.(container, pageContent) ??
|
||||
pageContent;
|
||||
const fit = calculateDiagramPageFit(
|
||||
intrinsic,
|
||||
availablePageContent
|
||||
);
|
||||
if (!fit.scaled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
container.dataset.pageHeightFitted = "true";
|
||||
container.style.setProperty("margin-block", "0", "important");
|
||||
if (!svg.getAttribute("viewBox")) {
|
||||
svg.setAttribute(
|
||||
"viewBox",
|
||||
`0 0 ${intrinsic.width} ${intrinsic.height}`
|
||||
);
|
||||
}
|
||||
svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
||||
svg.style.setProperty("display", "block");
|
||||
svg.style.setProperty("width", `${fit.width}px`, "important");
|
||||
svg.style.setProperty("height", `${fit.height}px`, "important");
|
||||
svg.style.setProperty("max-width", "100%", "important");
|
||||
svg.style.setProperty(
|
||||
"max-height",
|
||||
`${fit.height}px`,
|
||||
"important"
|
||||
);
|
||||
options.onFit?.(container, svg, fit);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user