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(options.containerSelector) ); for (const container of containers) { const svg = container.querySelector( 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); } }