import highlightCss from "highlight.js/styles/github.css?inline"; import katexCss from "katex/dist/katex.min.css?inline"; import echartsCss from "@md-to-pdf/markdown-echarts/styles.css?inline"; import type { PagedDocumentRenderResult, PagedDocumentTimings } from "@md-to-pdf/core"; import { Previewer } from "pagedjs"; import { fitOversizedEChartsToPage } from "./echarts-page-fit"; import { createMermaidSiteConfig } from "./mermaid-config"; import { fitOversizedMermaidToPage, getPageContentDimensions } from "./mermaid-page-fit"; import { renderMermaidDefinitions } from "./mermaid-renderer"; import { replaceMermaidSvgWithImages } from "./mermaid-static-image"; import type { MermaidOutputMode } from "./mermaid-static-image"; import { enablePrintMediaForPreview } from "./preview-styles"; import "./paged-table-handler"; import { buildPagedMediaCss, documentBaseCss, documentGeometryCss, formatPageNumber, type PagedPreviewPayload } from "./paged-preview"; import type { PagedRenderTarget } from "./paged-render-target"; export type { PagedDocumentRenderResult, PagedDocumentTimings } from "@md-to-pdf/core"; export interface PagedDocumentRenderOptions { target: PagedRenderTarget; shouldContinue?: () => boolean; mermaidOutput?: MermaidOutputMode; } function waitForImages(container: ParentNode) { const images = Array.from(container.querySelectorAll("img")); return Promise.all( images.map(async (image) => { if (image.complete) { return; } try { await image.decode(); } catch { await new Promise((resolve) => { image.addEventListener("load", () => resolve(), { once: true }); image.addEventListener("error", () => resolve(), { once: true }); }); } }) ); } function stylesheet( documentRef: Document, css: string, name: string ) { return { [`${documentRef.location.href}#${name}`]: css }; } function mountMeasurementContainer( documentRef: Document, content: DocumentFragment, payload: PagedPreviewPayload, target: PagedRenderTarget ) { const pageContent = getPageContentDimensions(payload.exportConfig); const style = documentRef.createElement("style"); const themeCss = target === "preview" ? enablePrintMediaForPreview(payload.themeCss) : payload.themeCss; style.dataset.pagedMeasurementStyle = "true"; style.textContent = [ documentBaseCss, highlightCss, katexCss, echartsCss, themeCss, documentGeometryCss ].join("\n"); const host = documentRef.createElement("div"); host.dataset.pagedMeasurementHost = "true"; host.style.cssText = [ "position: fixed", "left: -100000px", "top: 0", `width: ${pageContent.width}px`, "height: auto", "opacity: 0", "pointer-events: none", "contain: layout style paint" ].join(";"); documentRef.head.append(style); host.append(content); documentRef.body.append(host); return { host, restore() { content.append(...Array.from(host.childNodes)); host.remove(); style.remove(); } }; } function applyPageNumbers( container: ParentNode, payload: PagedPreviewPayload, totalPages: number ) { if (!payload.exportConfig.footer.enabled) { return; } const alignment = payload.exportConfig.footer.alignment; const pages = Array.from( container.querySelectorAll(".pagedjs_page") ); for (const [pageIndex, page] of pages.entries()) { const content = page.querySelector( `.pagedjs_margin-bottom-${alignment} .pagedjs_margin-content` ); if (!content) { continue; } content.textContent = formatPageNumber( payload.exportConfig.footer, pageIndex, totalPages ); content.setAttribute("data-page-number-rendered", "true"); } } function removeTrailingPdfPageBreak(container: ParentNode) { const pages = Array.from( container.querySelectorAll(".pagedjs_page") ); pages .at(-1) ?.style.setProperty("break-after", "auto", "important"); } export class PagedDocumentRuntime { private activePreviewer: Previewer | undefined; private mermaidRenderSequence = 0; private mermaidPromise: | Promise<(typeof import("mermaid"))["default"]> | undefined; private echartsPromise: | Promise< typeof import("@md-to-pdf/markdown-echarts/browser") > | undefined; constructor(private readonly root: HTMLElement) {} destroy() { if (!this.activePreviewer) { return; } this.activePreviewer.chunker.destroy(); this.activePreviewer.polisher.destroy(); this.activePreviewer = undefined; } private loadMermaid() { if (!this.mermaidPromise) { this.mermaidPromise = Promise.all([ import("mermaid"), import("@mermaid-js/layout-elk") ]).then( ([{ default: mermaid }, { default: elkLayouts }]) => { mermaid.registerLayoutLoaders(elkLayouts); return mermaid; } ); } return this.mermaidPromise; } private loadECharts() { if (!this.echartsPromise) { this.echartsPromise = import( "@md-to-pdf/markdown-echarts/browser" ); } return this.echartsPromise; } private async renderECharts( container: ParentNode, payload: PagedPreviewPayload ) { if (!payload.features.includes("echarts")) { return []; } const { renderEChartsBlocks } = await this.loadECharts(); const outcomes = await renderEChartsBlocks(container, { outputMode: "inline-svg", animation: false, concurrency: 2, timeoutMs: 5_000 }); return outcomes.flatMap((outcome, index) => outcome.success ? [] : [ `图表 ${index + 1}:${ outcome.error?.message ?? "未知错误" }` ] ); } private async renderMermaid( container: DocumentFragment, payload: PagedPreviewPayload ) { if (!payload.features.includes("mermaid")) { return []; } const nodes = Array.from( container.querySelectorAll( ".mermaid[data-mermaid-pending]" ) ); if (nodes.length === 0) { return []; } const mermaid = await this.loadMermaid(); mermaid.initialize( createMermaidSiteConfig(payload.exportConfig.mermaid) ); const outcomes = await renderMermaidDefinitions( nodes.map((node) => node.textContent ?? ""), async (definition) => { this.mermaidRenderSequence += 1; return mermaid.render( `mermaid-paged-document-${this.mermaidRenderSequence}`, definition ); } ); const errors: string[] = []; for (const [index, outcome] of outcomes.entries()) { const node = nodes[index]; if (!node) { continue; } node.removeAttribute("data-mermaid-pending"); if (outcome.success) { node.innerHTML = outcome.svg; outcome.bindFunctions?.(node); continue; } const message = outcome.error instanceof Error ? outcome.error.message : "未知错误"; node.classList.add("mermaid-error"); node.textContent = `Mermaid 图表 ${index + 1} 渲染失败:${message}`; errors.push(`图表 ${index + 1}:${message}`); } return errors; } async render( payload: PagedPreviewPayload, options: PagedDocumentRenderOptions ): Promise { const totalStartedAt = performance.now(); const shouldContinue = options.shouldContinue ?? (() => true); if (!shouldContinue()) { return undefined; } const setupStartedAt = performance.now(); const documentRef = this.root.ownerDocument; this.destroy(); this.root.replaceChildren(); documentRef.documentElement.dataset.renderTarget = options.target; documentRef.documentElement.lang = payload.metadata.language || "zh-CN"; documentRef.title = payload.metadata.title || "Markdown 分页文档"; const template = documentRef.createElement("template"); template.innerHTML = payload.articleHtml; const content = template.content; const setupMs = performance.now() - setupStartedAt; const mermaidStartedAt = performance.now(); const mermaidErrors = await this.renderMermaid(content, payload); const mermaidMs = performance.now() - mermaidStartedAt; let echartsErrors: string[] = []; let echartsMs = 0; let echartsFitMs = 0; let mermaidFitMs = 0; let mermaidConversionMs = 0; if ( payload.features.includes("echarts") || content.querySelector(".mermaid svg") ) { const measurement = mountMeasurementContainer( documentRef, content, payload, options.target ); try { await documentRef.fonts.ready; const echartsStartedAt = performance.now(); echartsErrors = await this.renderECharts( measurement.host, payload ); echartsMs = performance.now() - echartsStartedAt; await waitForImages(measurement.host); const echartsFitStartedAt = performance.now(); fitOversizedEChartsToPage( measurement.host, payload.exportConfig ); echartsFitMs = performance.now() - echartsFitStartedAt; const mermaidFitStartedAt = performance.now(); fitOversizedMermaidToPage( measurement.host, payload.exportConfig ); mermaidFitMs = performance.now() - mermaidFitStartedAt; const mermaidConversionStartedAt = performance.now(); if (options.mermaidOutput === "svg-image") { replaceMermaidSvgWithImages(measurement.host); await waitForImages(measurement.host); } mermaidConversionMs = performance.now() - mermaidConversionStartedAt; } finally { measurement.restore(); } } if (!shouldContinue()) { return undefined; } const resourceWaitStartedAt = performance.now(); await documentRef.fonts.ready; await waitForImages(content); const resourceWaitMs = performance.now() - resourceWaitStartedAt; const paginationStartedAt = performance.now(); const previewer = new Previewer(); this.activePreviewer = previewer; const flow = await previewer.preview( content, [ stylesheet(documentRef, documentBaseCss, "document-base"), stylesheet(documentRef, highlightCss, "highlight"), stylesheet(documentRef, katexCss, "katex"), stylesheet(documentRef, echartsCss, "echarts"), stylesheet(documentRef, payload.themeCss, "theme"), stylesheet( documentRef, documentGeometryCss, "document-geometry" ), stylesheet( documentRef, buildPagedMediaCss(payload.exportConfig, payload), "paged-media" ) ], this.root ); const paginationMs = performance.now() - paginationStartedAt; if (!shouldContinue()) { return undefined; } const finalizeStartedAt = performance.now(); applyPageNumbers(this.root, payload, flow.total); if (options.target === "pdf") { removeTrailingPdfPageBreak(this.root); } const finalizeMs = performance.now() - finalizeStartedAt; return { pageCount: flow.total, echartsErrors, mermaidErrors, timings: { setupMs, echartsMs, echartsFitMs, mermaidMs, mermaidFitMs, mermaidConversionMs, resourceWaitMs, paginationMs, finalizeMs, totalMs: performance.now() - totalStartedAt } }; } } declare global { interface Window { __mdToPdfRender?: ( payload: PagedPreviewPayload, target?: PagedRenderTarget ) => Promise; } }