import { getPaperDimensionsMm, type ExportConfig, type MarkdownDocumentMetadata, type PagedDocumentPayload } from "@md-to-pdf/core"; export const PAGED_PREVIEW_MESSAGE_SCOPE = "md-to-pdf:paged-preview"; export type PreviewMetadata = MarkdownDocumentMetadata; export type PagedPreviewPayload = PagedDocumentPayload; export interface PagedPreviewRenderRequest { scope: typeof PAGED_PREVIEW_MESSAGE_SCOPE; type: "render"; requestId: number; payload: PagedPreviewPayload; } export type PagedPreviewFrameMessage = | { scope: typeof PAGED_PREVIEW_MESSAGE_SCOPE; type: "ready"; } | { scope: typeof PAGED_PREVIEW_MESSAGE_SCOPE; type: "rendering"; requestId: number; } | { scope: typeof PAGED_PREVIEW_MESSAGE_SCOPE; type: "rendered"; requestId: number; pageCount: number; contentHeight: number; echartsErrors: string[]; mermaidErrors: string[]; } | { scope: typeof PAGED_PREVIEW_MESSAGE_SCOPE; type: "error"; requestId: number; message: string; }; export const documentBaseCss = ` :root { color-scheme: light; background: transparent; } * { box-sizing: border-box; } html, body { width: 100%; min-width: 0; margin: 0; padding: 0; overflow: hidden; background: transparent; } img, svg { max-width: 100%; } #write table { width: 100%; table-layout: auto; } #write th, #write td { overflow-wrap: anywhere; } .mermaid { display: flex; justify-content: center; margin: 1.5em 0; overflow: visible; } .mermaid-error { display: block; padding: 0.85em 1em; border: 1px solid #dc2626; color: #991b1b; background: #fef2f2; white-space: pre-wrap; } .md-echarts { break-inside: avoid; page-break-inside: avoid; } `; export const documentGeometryCss = ` html, body, #preview-root { background: transparent !important; } #write { width: 100% !important; max-width: none !important; margin: 0 !important; padding: 0 !important; } `; function cssString(value: string) { return JSON.stringify(value) .replace(/\u2028/g, "\\2028 ") .replace(/\u2029/g, "\\2029 "); } function resolveHeaderContent( template: string, payload: Pick ) { const values: Record = { title: payload.metadata.title, author: payload.metadata.author, filename: payload.fileName }; return template.replace( /\$\{(title|author|filename)\}/g, (_, name: string) => values[name] ?? "" ); } export function formatPageNumber( config: ExportConfig["footer"], pageIndex: number, totalPages: number ) { const page = config.startFrom + pageIndex; if (config.format === "page") { return String(page); } if (config.format === "page-total") { return `${page} / ${totalPages}`; } if (config.format === "chinese-page-total") { return `第 ${page} 页 / 共 ${totalPages} 页`; } if (config.format === "dash-page") { return `- ${page} -`; } return (config.template || "${page} / ${pages}") .replace(/\$\{page\}/g, String(page)) .replace(/\$\{pages\}/g, String(totalPages)); } function marginBox( position: "top" | "bottom", alignment: "left" | "center" | "right", content: string, options: { color: string; fontSize: string; height: string; showDivider: boolean; } ) { const border = options.showDivider && position === "top" ? "border-bottom: 0.2mm solid currentColor;" : options.showDivider ? "border-top: 0.2mm solid currentColor;" : ""; const verticalAlignment = position === "top" ? "bottom" : "top"; return ` @${position}-${alignment} { content: ${content}; height: ${options.height}; color: ${options.color}; font-family: inherit; font-size: ${options.fontSize}; font-weight: 400; line-height: 1.25; text-align: ${alignment}; vertical-align: ${verticalAlignment}; ${border} }`; } function buildHeaderCss( config: ExportConfig, payload: Pick ) { if (!config.header.enabled) { return ""; } const options = { color: config.header.color, fontSize: config.header.fontSize, height: config.header.height, showDivider: config.header.showDivider }; return (["left", "center", "right"] as const) .map((alignment) => { const slot = config.header[alignment]; const value = slot.enabled ? resolveHeaderContent(slot.content, payload) : ""; return marginBox( "top", alignment, cssString(value), options ); }) .join("\n"); } function buildFooterCss(config: ExportConfig) { if (!config.footer.enabled) { return ""; } const options = { color: config.footer.color, fontSize: config.footer.fontSize, height: config.footer.height, showDivider: config.footer.showDivider }; return (["left", "center", "right"] as const) .map((alignment) => marginBox( "bottom", alignment, alignment === config.footer.alignment ? cssString("") : cssString(""), options ) ) .join("\n"); } export function buildPagedMediaCss( config: ExportConfig, payload: Pick ) { const dimensions = getPaperDimensionsMm( config.paper.format, config.paper.orientation ); return ` @page { size: ${dimensions.width}mm ${dimensions.height}mm; margin: ${config.paper.margins.top} ${config.paper.margins.right} ${config.paper.margins.bottom} ${config.paper.margins.left}; ${buildHeaderCss(config, payload)} ${buildFooterCss(config)} } #write p, #write li { orphans: 3; widows: 3; } #write h1, #write h2, #write h3, #write h4, #write h5, #write h6 { break-after: avoid; break-inside: avoid; } #write thead { display: table-header-group; } #write tfoot { display: table-footer-group; } #write tr, #write pre, #write blockquote, #write .katex-display, #write .mermaid, #write .md-echarts { break-inside: avoid; } #write table[data-empty-split-table="true"] { display: none; } #write img, #write svg { break-inside: avoid; page-break-inside: avoid; } ${ config.print.pageBreakBeforeH1 ? `#write h1:not(:first-child) { break-before: page; }` : "" } .pagedjs_pages { display: flex; flex-direction: column; align-items: center; gap: 24px; width: max-content; min-width: 100%; margin: 0 auto; padding: 34px 0; } html[data-render-target="preview"] { height: 100%; overflow-x: hidden; overflow-y: hidden; } html[data-render-target="preview"] body { min-height: 100%; overflow: visible; } .pagedjs_page { flex: none; margin: 0 !important; background: #fff; box-shadow: 0 18px 48px rgb(37 49 43 / 16%); } html[data-render-target="pdf"], html[data-render-target="pdf"] body { height: auto !important; min-height: 0 !important; overflow: visible !important; background: #fff !important; } html[data-render-target="pdf"] .pagedjs_pages { display: block; width: auto; min-width: 0; padding: 0; } html[data-render-target="pdf"] .pagedjs_page { box-shadow: none; break-after: page; } `; } export function createPagedPreviewRenderRequest( requestId: number, payload: PagedPreviewPayload ): PagedPreviewRenderRequest { return { scope: PAGED_PREVIEW_MESSAGE_SCOPE, type: "render", requestId, payload }; } export function isPagedPreviewRenderRequest( value: unknown ): value is PagedPreviewRenderRequest { if (!value || typeof value !== "object") { return false; } const candidate = value as Partial; return ( candidate.scope === PAGED_PREVIEW_MESSAGE_SCOPE && candidate.type === "render" && Number.isInteger(candidate.requestId) && Boolean(candidate.payload) && typeof candidate.payload?.articleHtml === "string" && typeof candidate.payload?.themeCss === "string" ); } export function isPagedPreviewFrameMessage( value: unknown ): value is PagedPreviewFrameMessage { if (!value || typeof value !== "object") { return false; } const candidate = value as Partial; if ( candidate.scope !== PAGED_PREVIEW_MESSAGE_SCOPE || !["ready", "rendering", "rendered", "error"].includes( candidate.type ?? "" ) ) { return false; } if (candidate.type !== "rendered") { return true; } return ( Number.isInteger(candidate.requestId) && Number.isInteger(candidate.pageCount) && typeof candidate.contentHeight === "number" && Number.isFinite(candidate.contentHeight) && candidate.contentHeight > 0 && Array.isArray(candidate.echartsErrors) && Array.isArray(candidate.mermaidErrors) ); }