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