Files
MorphDoc/apps/web/src/diagram-page-fit.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

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);
}
}