- {previewDocument ? (
+ {previewPayload ? (
diff --git a/apps/web/src/mermaid-page-fit.ts b/apps/web/src/mermaid-page-fit.ts
new file mode 100644
index 0000000..84a3e7b
--- /dev/null
+++ b/apps/web/src/mermaid-page-fit.ts
@@ -0,0 +1,123 @@
+import {
+ getPaperDimensionsMm,
+ lengthToMillimeters,
+ millimetersToCssPixels,
+ type ExportConfig
+} from "@md-to-pdf/core";
+
+export interface MermaidDimensions {
+ width: number;
+ height: number;
+}
+
+export interface MermaidPageFit extends MermaidDimensions {
+ scaled: boolean;
+}
+
+const PAGE_FIT_EPSILON_PX = 1;
+
+export function parseSvgViewBox(
+ value: string | null
+): MermaidDimensions | 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 };
+}
+
+export function getPageContentDimensions(
+ config: ExportConfig
+): MermaidDimensions {
+ 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 calculateMermaidPageFit(
+ intrinsic: MermaidDimensions,
+ pageContent: MermaidDimensions
+): MermaidPageFit {
+ 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 fitOversizedMermaidToPage(
+ container: ParentNode,
+ config: ExportConfig
+) {
+ const pageContent = getPageContentDimensions(config);
+ const diagrams = Array.from(
+ container.querySelectorAll
(".mermaid")
+ );
+
+ for (const diagram of diagrams) {
+ const svg = diagram.querySelector("svg");
+ const intrinsic = parseSvgViewBox(svg?.getAttribute("viewBox") ?? null);
+ if (!svg || !intrinsic) {
+ continue;
+ }
+
+ const fit = calculateMermaidPageFit(intrinsic, pageContent);
+ if (!fit.scaled) {
+ continue;
+ }
+
+ diagram.dataset.pageHeightFitted = "true";
+ diagram.style.setProperty("margin-block", "0", "important");
+ 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");
+ }
+}
diff --git a/apps/web/src/paged-preview-frame.ts b/apps/web/src/paged-preview-frame.ts
new file mode 100644
index 0000000..b67d7ea
--- /dev/null
+++ b/apps/web/src/paged-preview-frame.ts
@@ -0,0 +1,279 @@
+import highlightCss from "highlight.js/styles/github.css?inline";
+import katexCss from "katex/dist/katex.min.css?inline";
+import { Previewer } from "pagedjs";
+import { createMermaidSiteConfig } from "./mermaid-config";
+import { fitOversizedMermaidToPage } from "./mermaid-page-fit";
+import { renderMermaidDefinitions } from "./mermaid-renderer";
+import "./paged-table-handler";
+import {
+ buildPagedMediaCss,
+ documentBaseCss,
+ documentGeometryCss,
+ formatPageNumber,
+ isPagedPreviewRenderRequest,
+ PAGED_PREVIEW_MESSAGE_SCOPE,
+ type PagedPreviewFrameMessage,
+ type PagedPreviewPayload
+} from "./paged-preview";
+
+function getPreviewRoot() {
+ const element = document.querySelector("#preview-root");
+ if (!element) {
+ throw new Error("分页预览容器不存在");
+ }
+ return element;
+}
+
+const previewRoot = getPreviewRoot();
+
+let latestRequestId = 0;
+let renderQueue = Promise.resolve();
+let activePreviewer: Previewer | undefined;
+let mermaidRenderSequence = 0;
+let mermaidPromise: Promise<
+ (typeof import("mermaid"))["default"]
+> | undefined;
+
+function post(message: PagedPreviewFrameMessage) {
+ window.parent.postMessage(message, window.location.origin);
+}
+
+function destroyActivePreviewer() {
+ if (!activePreviewer) {
+ return;
+ }
+
+ activePreviewer.chunker.destroy();
+ activePreviewer.polisher.destroy();
+ activePreviewer = undefined;
+}
+
+function loadMermaid() {
+ if (!mermaidPromise) {
+ mermaidPromise = import("mermaid").then(({ default: mermaid }) => {
+ mermaid.initialize(createMermaidSiteConfig());
+ return mermaid;
+ });
+ }
+
+ return mermaidPromise;
+}
+
+async function 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 loadMermaid();
+ const outcomes = await renderMermaidDefinitions(
+ nodes.map((node) => node.textContent ?? ""),
+ async (definition) => {
+ mermaidRenderSequence += 1;
+ return mermaid.render(
+ `mermaid-paged-preview-${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 function waitForImages(container: ParentNode) {
+ const images = Array.from(container.querySelectorAll("img"));
+ await 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(css: string, name: string) {
+ return {
+ [`${window.location.href}#${name}`]: css
+ };
+}
+
+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");
+ }
+}
+
+async function renderPagedPreview(
+ requestId: number,
+ payload: PagedPreviewPayload
+) {
+ if (requestId !== latestRequestId) {
+ return;
+ }
+
+ post({
+ scope: PAGED_PREVIEW_MESSAGE_SCOPE,
+ type: "rendering",
+ requestId
+ });
+
+ destroyActivePreviewer();
+ previewRoot.replaceChildren();
+ document.documentElement.lang = payload.metadata.language || "zh-CN";
+ document.title = payload.metadata.title || "Markdown 分页预览";
+
+ const template = document.createElement("template");
+ template.innerHTML = payload.articleHtml;
+ const content = template.content;
+ const mermaidErrors = await renderMermaid(content, payload);
+ fitOversizedMermaidToPage(content, payload.exportConfig);
+
+ if (requestId !== latestRequestId) {
+ return;
+ }
+
+ await document.fonts.ready;
+ await waitForImages(content);
+
+ const previewer = new Previewer();
+ activePreviewer = previewer;
+ const flow = await previewer.preview(
+ content,
+ [
+ stylesheet(documentBaseCss, "document-base"),
+ stylesheet(highlightCss, "highlight"),
+ stylesheet(katexCss, "katex"),
+ stylesheet(payload.themeCss, "theme"),
+ stylesheet(documentGeometryCss, "document-geometry"),
+ stylesheet(
+ buildPagedMediaCss(payload.exportConfig, payload),
+ "paged-media"
+ )
+ ],
+ previewRoot
+ );
+ applyPageNumbers(previewRoot, payload, flow.total);
+
+ if (requestId !== latestRequestId) {
+ return;
+ }
+
+ const contentHeight = Math.ceil(
+ Math.max(
+ document.documentElement.scrollHeight,
+ document.body.scrollHeight,
+ previewRoot.scrollHeight
+ )
+ );
+
+ post({
+ scope: PAGED_PREVIEW_MESSAGE_SCOPE,
+ type: "rendered",
+ requestId,
+ pageCount: flow.total,
+ contentHeight,
+ mermaidErrors
+ });
+}
+
+window.addEventListener("message", (event: MessageEvent) => {
+ if (
+ event.origin !== window.location.origin ||
+ event.source !== window.parent ||
+ !isPagedPreviewRenderRequest(event.data)
+ ) {
+ return;
+ }
+
+ latestRequestId = Math.max(latestRequestId, event.data.requestId);
+ const { requestId, payload } = event.data;
+ renderQueue = renderQueue
+ .catch(() => undefined)
+ .then(async () => {
+ try {
+ await renderPagedPreview(requestId, payload);
+ } catch (reason: unknown) {
+ if (requestId !== latestRequestId) {
+ return;
+ }
+ post({
+ scope: PAGED_PREVIEW_MESSAGE_SCOPE,
+ type: "error",
+ requestId,
+ message:
+ reason instanceof Error ? reason.message : "分页预览失败"
+ });
+ }
+ });
+});
+
+post({
+ scope: PAGED_PREVIEW_MESSAGE_SCOPE,
+ type: "ready"
+});
diff --git a/apps/web/src/paged-preview.ts b/apps/web/src/paged-preview.ts
new file mode 100644
index 0000000..594ad66
--- /dev/null
+++ b/apps/web/src/paged-preview.ts
@@ -0,0 +1,414 @@
+import {
+ getPaperDimensionsMm,
+ type ExportConfig
+} from "@md-to-pdf/core";
+
+export const PAGED_PREVIEW_MESSAGE_SCOPE = "md-to-pdf:paged-preview";
+
+export interface PreviewMetadata {
+ title: string;
+ author: string;
+ subject: string;
+ keywords: string[];
+ language: string;
+}
+
+export interface PagedPreviewPayload {
+ articleHtml: string;
+ fileName: string;
+ metadata: PreviewMetadata;
+ features: string[];
+ themeCss: string;
+ exportConfig: ExportConfig;
+}
+
+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;
+ 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;
+}
+`;
+
+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 {
+ 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: 0;
+}
+
+.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 {
+ background: #fff !important;
+}
+
+html[data-render-target="pdf"] .pagedjs_pages {
+ display: block;
+ width: auto;
+ min-width: 0;
+}
+
+html[data-render-target="pdf"] .pagedjs_page {
+ box-shadow: none;
+ break-after: page;
+}
+
+html[data-render-target="pdf"] .pagedjs_page:last-child {
+ break-after: auto;
+}
+`;
+}
+
+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;
+ return (
+ candidate.scope === PAGED_PREVIEW_MESSAGE_SCOPE &&
+ ["ready", "rendering", "rendered", "error"].includes(
+ candidate.type ?? ""
+ )
+ );
+}
diff --git a/apps/web/src/paged-table-handler.ts b/apps/web/src/paged-table-handler.ts
new file mode 100644
index 0000000..2f7bb0b
--- /dev/null
+++ b/apps/web/src/paged-table-handler.ts
@@ -0,0 +1,135 @@
+import { Handler, registerHandlers } from "pagedjs";
+
+interface PagedBreakToken {
+ node?: Element;
+}
+
+interface PagedChunker {
+ source: ParentNode;
+}
+
+interface PagedHandlerContext {
+ chunker: PagedChunker;
+}
+
+function elementAncestors(
+ element: Element,
+ selector: string
+) {
+ const ancestors: Element[] = [];
+ let current = element.parentElement;
+
+ while (current) {
+ if (current.matches(selector)) {
+ ancestors.unshift(current);
+ }
+ current = current.parentElement;
+ }
+
+ return ancestors;
+}
+
+class RepeatTableHeadersHandler extends Handler {
+ declare chunker: PagedChunker;
+ private splitTableRefs: string[] = [];
+
+ constructor(
+ chunker: unknown,
+ polisher: unknown,
+ caller: unknown
+ ) {
+ super(chunker, polisher, caller);
+ }
+
+ afterPageLayout(
+ pageElement: HTMLElement,
+ _page: unknown,
+ breakToken?: PagedBreakToken
+ ) {
+ this.splitTableRefs = [];
+ const node = breakToken?.node;
+ if (!node) {
+ return;
+ }
+
+ const tables = elementAncestors(node, "table");
+ if (node.matches("table")) {
+ tables.push(node);
+ }
+
+ this.splitTableRefs = Array.from(
+ new Set(
+ tables
+ .map((table) => table.getAttribute("data-ref"))
+ .filter((ref): ref is string => Boolean(ref))
+ )
+ );
+
+ for (const ref of this.splitTableRefs) {
+ const renderedTable =
+ pageElement.querySelector(
+ `table[data-ref="${CSS.escape(ref)}"]`
+ );
+ if (!renderedTable?.querySelector("tbody > tr")) {
+ renderedTable?.setAttribute(
+ "data-empty-split-table",
+ "true"
+ );
+ }
+ }
+ }
+
+ layout(rendered: HTMLElement) {
+ for (const ref of this.splitTableRefs) {
+ const renderedTable =
+ rendered.querySelector(
+ `table[data-ref="${CSS.escape(ref)}"]`
+ );
+ if (
+ !renderedTable ||
+ renderedTable.hasAttribute("data-repeated-header")
+ ) {
+ continue;
+ }
+
+ const sourceTable =
+ this.chunker.source.querySelector(
+ `table[data-ref="${CSS.escape(ref)}"]`
+ );
+ if (!sourceTable) {
+ continue;
+ }
+
+ const firstChild = renderedTable.firstChild;
+ for (const colgroup of sourceTable.querySelectorAll("colgroup")) {
+ renderedTable.insertBefore(
+ colgroup.cloneNode(true),
+ firstChild
+ );
+ }
+
+ if (!renderedTable.querySelector("thead")) {
+ const sourceHeader = sourceTable.querySelector("thead");
+ if (sourceHeader) {
+ renderedTable.insertBefore(
+ sourceHeader.cloneNode(true),
+ renderedTable.firstChild
+ );
+ }
+ }
+
+ renderedTable.setAttribute("data-repeated-header", "true");
+ }
+ }
+}
+
+const handlerRegistration = globalThis as typeof globalThis & {
+ __mdToPdfTableHandlerRegistered?: boolean;
+};
+
+if (!handlerRegistration.__mdToPdfTableHandlerRegistered) {
+ registerHandlers(RepeatTableHeadersHandler);
+ handlerRegistration.__mdToPdfTableHandlerRegistered = true;
+}
+
+export { RepeatTableHeadersHandler };
diff --git a/apps/web/src/pagedjs.d.ts b/apps/web/src/pagedjs.d.ts
new file mode 100644
index 0000000..ba861c7
--- /dev/null
+++ b/apps/web/src/pagedjs.d.ts
@@ -0,0 +1,41 @@
+declare module "pagedjs" {
+ export interface PagedFlow {
+ total: number;
+ pages: HTMLElement[];
+ performance: number;
+ size: {
+ width: { value: number; unit: string };
+ height: { value: number; unit: string };
+ format?: string;
+ orientation?: string;
+ };
+ }
+
+ export type PagedStylesheet = string | Record;
+
+ export class Previewer {
+ chunker: {
+ destroy(): void;
+ };
+ polisher: {
+ destroy(): void;
+ };
+ preview(
+ content?: HTMLElement | DocumentFragment | string,
+ stylesheets?: PagedStylesheet[],
+ renderTo?: HTMLElement | string
+ ): Promise;
+ }
+
+ export class Handler {
+ constructor(
+ chunker: unknown,
+ polisher: unknown,
+ caller: unknown
+ );
+ }
+
+ export function registerHandlers(
+ ...handlers: Array
+ ): void;
+}
diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css
index 4ad3623..09525bd 100644
--- a/apps/web/src/styles.css
+++ b/apps/web/src/styles.css
@@ -47,7 +47,9 @@ button:disabled {
.workspace {
display: flex;
flex-direction: column;
- min-height: 100vh;
+ height: 100vh;
+ min-height: 0;
+ overflow: hidden;
}
.topbar {
@@ -109,7 +111,7 @@ h1 {
display: flex;
flex-direction: column;
min-width: 0;
- min-height: calc(100vh - 92px);
+ min-height: 0;
}
.editor-panel {
@@ -216,7 +218,7 @@ h1 {
textarea {
flex: 1;
width: 100%;
- min-height: 560px;
+ min-height: 0;
padding: 24px;
border: 0;
outline: 0;
@@ -249,8 +251,7 @@ textarea:focus {
.paper {
margin: 0 auto;
- background: #fff;
- box-shadow: 0 18px 48px rgb(37 49 43 / 16%);
+ background: transparent;
}
.preview-frame {
@@ -258,7 +259,7 @@ textarea:focus {
width: 100%;
min-height: 500px;
border: 0;
- background: #fff;
+ background: transparent;
}
.preview-loading {
@@ -541,6 +542,12 @@ textarea:focus {
}
@media (max-width: 900px) {
+ .workspace {
+ height: auto;
+ min-height: 100vh;
+ overflow: visible;
+ }
+
.topbar {
align-items: flex-start;
padding: 16px;
@@ -559,6 +566,10 @@ textarea:focus {
min-height: 650px;
}
+ textarea {
+ min-height: 560px;
+ }
+
.editor-panel {
border-right: 0;
border-bottom: 1px solid #cbd2ce;
diff --git a/apps/web/tests/mermaid-page-fit.test.ts b/apps/web/tests/mermaid-page-fit.test.ts
new file mode 100644
index 0000000..652107c
--- /dev/null
+++ b/apps/web/tests/mermaid-page-fit.test.ts
@@ -0,0 +1,53 @@
+import { defaultExportConfig } from "@md-to-pdf/core";
+import { describe, expect, it } from "vitest";
+import {
+ calculateMermaidPageFit,
+ getPageContentDimensions,
+ parseSvgViewBox
+} from "../src/mermaid-page-fit";
+
+describe("Mermaid 单页高度适配", () => {
+ it("解析 SVG viewBox 尺寸", () => {
+ expect(parseSvgViewBox("0 0 1200 2400")).toEqual({
+ width: 1200,
+ height: 2400
+ });
+ expect(parseSvgViewBox("0,0,800,600")).toEqual({
+ width: 800,
+ height: 600
+ });
+ expect(parseSvgViewBox("0 0 0 600")).toBeUndefined();
+ expect(parseSvgViewBox(null)).toBeUndefined();
+ });
+
+ it("使用纸张和页边距计算正文区域", () => {
+ const dimensions = getPageContentDimensions(defaultExportConfig);
+
+ expect(dimensions.width).toBeCloseTo(672.7559, 3);
+ expect(dimensions.height).toBeCloseTo(1001.5748, 3);
+ });
+
+ it("宽度适配后未超高时保持普通 Mermaid 布局", () => {
+ const fit = calculateMermaidPageFit(
+ { width: 1600, height: 900 },
+ { width: 672, height: 1000 }
+ );
+
+ expect(fit).toEqual({
+ width: 672,
+ height: 378,
+ scaled: false
+ });
+ });
+
+ it("宽度适配后超高时按页面高度等比例缩小", () => {
+ const fit = calculateMermaidPageFit(
+ { width: 1000, height: 2000 },
+ { width: 672, height: 1000 }
+ );
+
+ expect(fit.scaled).toBe(true);
+ expect(fit.height).toBe(999);
+ expect(fit.width).toBe(499.5);
+ });
+});
diff --git a/apps/web/tests/paged-preview.test.ts b/apps/web/tests/paged-preview.test.ts
new file mode 100644
index 0000000..bee3d52
--- /dev/null
+++ b/apps/web/tests/paged-preview.test.ts
@@ -0,0 +1,185 @@
+import { defaultExportConfig } from "@md-to-pdf/core";
+import { describe, expect, it } from "vitest";
+import {
+ buildPagedMediaCss,
+ createPagedPreviewRenderRequest,
+ documentGeometryCss,
+ formatPageNumber,
+ isPagedPreviewFrameMessage,
+ isPagedPreviewRenderRequest,
+ PAGED_PREVIEW_MESSAGE_SCOPE,
+ type PagedPreviewPayload
+} from "../src/paged-preview";
+
+const payload: PagedPreviewPayload = {
+ articleHtml: '分页测试
',
+ fileName: "分页测试.md",
+ metadata: {
+ title: "分页测试",
+ author: "",
+ subject: "",
+ keywords: [],
+ language: "zh-CN"
+ },
+ features: [],
+ themeCss: "#write { color: #333; }",
+ exportConfig: defaultExportConfig
+};
+
+describe("分页预览协议", () => {
+ it("生成真实纸张尺寸和页边距 CSS", () => {
+ const css = buildPagedMediaCss(defaultExportConfig, payload);
+
+ expect(css).toContain("size: 210mm 297mm");
+ expect(css).toContain("margin: 16mm 16mm");
+ expect(css).toContain("@bottom-center");
+ expect(css).not.toContain("counter-reset: page");
+ expect(css).toContain("#write thead");
+ expect(css).toContain("display: table-header-group");
+ expect(css).toContain("break-inside: avoid");
+ expect(css).toContain(".pagedjs_pages");
+ expect(css).toContain('html[data-render-target="pdf"]');
+ expect(css).toContain("break-after: page");
+ expect(css).not.toContain("@media print");
+ });
+
+ it("安全替换页眉变量并生成三栏页边距盒", () => {
+ const css = buildPagedMediaCss(
+ {
+ ...defaultExportConfig,
+ header: {
+ ...defaultExportConfig.header,
+ enabled: true,
+ showDivider: true,
+ left: {
+ enabled: true,
+ content: '${title} — ${filename} "测试"'
+ },
+ center: {
+ enabled: true,
+ content: "${author}"
+ }
+ }
+ },
+ {
+ ...payload,
+ metadata: {
+ ...payload.metadata,
+ author: "内网团队"
+ }
+ }
+ );
+
+ expect(css).toContain("@top-left");
+ expect(css).toContain("@top-center");
+ expect(css).toContain("@top-right");
+ expect(css).toContain(
+ 'content: "分页测试 — 分页测试.md \\"测试\\""'
+ );
+ expect(css).toContain('content: "内网团队"');
+ expect(css).toContain("border-bottom: 0.2mm solid currentColor");
+ });
+
+ it("支持自定义页码模板和起始页码", () => {
+ const css = buildPagedMediaCss(
+ {
+ ...defaultExportConfig,
+ footer: {
+ ...defaultExportConfig.footer,
+ format: "custom",
+ template: "第 ${page} / ${pages} 页",
+ alignment: "right",
+ startFrom: 5
+ }
+ },
+ payload
+ );
+
+ expect(css).toContain("@bottom-right");
+ expect(
+ formatPageNumber(
+ {
+ ...defaultExportConfig.footer,
+ format: "custom",
+ template: "第 ${page} / ${pages} 页",
+ startFrom: 5
+ },
+ 2,
+ 8
+ )
+ ).toBe("第 7 / 8 页");
+ });
+
+ it("逐页递增内置页码格式但保持真实总页数", () => {
+ expect(
+ formatPageNumber(defaultExportConfig.footer, 0, 5)
+ ).toBe("1 / 5");
+ expect(
+ formatPageNumber(defaultExportConfig.footer, 1, 5)
+ ).toBe("2 / 5");
+ expect(
+ formatPageNumber(
+ {
+ ...defaultExportConfig.footer,
+ format: "chinese-page-total",
+ startFrom: 5
+ },
+ 3,
+ 5
+ )
+ ).toBe("第 8 页 / 共 5 页");
+ expect(
+ formatPageNumber(
+ {
+ ...defaultExportConfig.footer,
+ format: "dash-page"
+ },
+ 4,
+ 5
+ )
+ ).toBe("- 5 -");
+ });
+
+ it("在主题 CSS 之后强制分页画布保持透明", () => {
+ expect(documentGeometryCss).toContain(
+ "background: transparent !important"
+ );
+ });
+
+ it("生成并识别分页渲染请求", () => {
+ const request = createPagedPreviewRenderRequest(7, payload);
+
+ expect(request).toEqual({
+ scope: PAGED_PREVIEW_MESSAGE_SCOPE,
+ type: "render",
+ requestId: 7,
+ payload
+ });
+ expect(isPagedPreviewRenderRequest(request)).toBe(true);
+ expect(
+ isPagedPreviewRenderRequest({
+ ...request,
+ requestId: "7"
+ })
+ ).toBe(false);
+ });
+
+ it("只接受分页 iframe 协议消息", () => {
+ expect(
+ isPagedPreviewFrameMessage({
+ scope: PAGED_PREVIEW_MESSAGE_SCOPE,
+ type: "rendered",
+ requestId: 3,
+ pageCount: 2,
+ contentHeight: 2300,
+ mermaidErrors: []
+ })
+ ).toBe(true);
+ expect(
+ isPagedPreviewFrameMessage({
+ scope: "other",
+ type: "ready"
+ })
+ ).toBe(false);
+ });
+});
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
index c4fc657..bf606e3 100644
--- a/apps/web/vite.config.ts
+++ b/apps/web/vite.config.ts
@@ -1,8 +1,19 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
+import { fileURLToPath } from "node:url";
export default defineConfig({
plugins: [react()],
+ build: {
+ rollupOptions: {
+ input: {
+ main: fileURLToPath(new URL("./index.html", import.meta.url)),
+ previewFrame: fileURLToPath(
+ new URL("./preview-frame.html", import.meta.url)
+ )
+ }
+ }
+ },
server: {
port: 5173,
proxy: {
diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md
index 5ca01f3..c81b1c4 100644
--- a/docs/PROGRESS.md
+++ b/docs/PROGRESS.md
@@ -1,6 +1,6 @@
# Markdown PDF 导出器进度
-最后更新:2026-07-25
+最后更新:2026-07-26
## 1. 当前概况
@@ -19,13 +19,14 @@ main
已有提交:
```text
+52cf816 feat: 完善导出设置与打印预览
ec14f79 feat: 扩展本地主题兼容能力
fa07472 feat: 实现网页实时预览
7224dfd feat: 实现 Markdown 渲染核心
ec48bce chore: 初始化项目骨架
```
-当前工作区存在未提交修改,主要是导出配置版本 2、导出设置抽屉、浏览器缓存和实时纸张尺寸及页边距预览。接手时必须保留并审查这些修改,不要重置工作区。
+当前工作区存在未提交修改,主要是基于 Paged.js 的真实分页预览、页眉页码、跨页表格和超高 Mermaid 图表适配。接手时必须保留并审查这些修改,不要重置工作区。
## 2. 已完成
@@ -119,53 +120,58 @@ ec48bce chore: 初始化项目骨架
- 主题开发指南;
- 后端测试增至 6 项。
+### 2.7 导出设置与打印预览
+
+提交 `52cf816` 已完成:
+
+- 导出配置升级到版本 2;
+- 纸张收敛为 A3、A4、A5、US-Letter 和 US-Legal;
+- 默认 A4 纵向和 16mm 四边页边距;
+- 统一 96 CSS px/in、72 PDF pt/in 和 25.4mm/in 的物理单位换算;
+- 响应式导出设置抽屉;
+- 纸张、方向、页边距、页眉、页码和主题的版本化浏览器缓存;
+- 页眉左中右内容和变量;
+- 五种页码样式、对齐和起始页码;
+- Mermaid 官方配置 frontmatter、默认主题与错误隔离;
+- Markdown 表格对齐属性保留和自适应列宽;
+- 纸张尺寸和页边距实时预览。
+
## 3. 当前未提交工作
以下内容已写入工作区,但尚未提交:
-### 3.1 导出配置版本 2
+### 3.1 Paged.js 真实分页预览
-- 纸张只保留 A3、A4、A5、US-Letter 和 US-Legal。
-- 固定纸张尺寸分别为 297×420、210×297、148×210、216×279 和 216×356mm。
-- 默认 A4 纵向,四边页边距均为 16mm。
-- 统一采用 96 CSS px/in、72 PDF pt/in 和 25.4mm/in 的物理单位换算。
-- 保留横向和纵向。
-- 页脚模型收敛为页码设置,支持五种页码样式、对齐和起始页码。
-- 校验页边距之和必须为正文保留正尺寸区域。
-- 增加 6 项共享配置测试。
+- 引入 MIT 许可证的 Paged.js 0.4.3。
+- 使用独立同源沙箱 iframe 执行分页,父页面通过严格消息协议传递渲染载荷。
+- 每张纸以真实物理尺寸独立显示,页面间保留 24px 间隔和预览阴影。
+- 主题 CSS 原样交给 Paged.js,使主题 `@media print` 规则与未来 PDF 一致。
+- GitHub 主题打印字号已与 Typora PDF 对齐:正文 13px、一级标题 29.25px、二级标题 22.75px。
+- W30 对照文档在 Typora 和网页预览中均为 4 页。
+- 主题切换会重置 iframe 就绪握手,GitHub、Pixyll 和 Whitey 可连续切换且无需刷新。
-### 3.2 导出设置和浏览器缓存
+### 3.2 页眉、页码与跨页内容
-- 增加响应式右侧导出设置抽屉。
-- 支持五种纸张、方向和四边页边距。
-- 已提供页眉左中右内容、页脚页码样式、位置、起始页码和分隔线控件。
-- 配置和主题选择写入版本化 `localStorage`,不缓存 Markdown 正文。
-- 缓存缺失、损坏或版本过期时恢复默认配置。
-- 切换到更小纸张时自动等比例收敛过大的旧页边距。
-- 增加 3 项浏览器缓存测试和 3 项打印媒体预览测试。
+- 页眉支持左中右三栏、标题、作者和文件名变量及分隔线。
+- 页脚支持五种页码格式、左中右位置、起始页码和分隔线。
+- 当前页码逐页递增,总页数使用真实分页结果。
+- 标题避免孤立在页尾,段落使用孤行和寡行约束。
+- 长表格允许跨页,并通过 Paged.js 处理器重复表头。
+- 表格行、代码块、引用、公式、图片和 Mermaid 默认避免从中间断开。
-### 3.3 实时纸张预览
+### 3.3 Mermaid 单页适配
-- 纸张尺寸、方向和四边页边距实时作用于预览。
-- 删除本地主题专用的 8mm 外边距特例。
-- 在主题 CSS 后追加共享几何 CSS,统一清除 `body` 和 `#write` 的页面宽度、内外边距限制。
-- 网页预览将主题的 `@media print` 规则按原始位置转换为屏幕预览规则,使 Typora GitHub 等主题使用与 PDF 相同的打印字号和行高。
-- 固定逻辑页面后续按 96 CSS px/in 分页,预览显示倍率只缩放外层,不参与正文换行和分页计算。
-- 页眉和页码配置将在下一阶段分页预览中显示。
-
-### 3.4 Mermaid 配置与错误隔离
-
-- 使用 Mermaid 官方 npm 包,当前锁定安装版本为 11.16.0。
-- 无区块配置时显式使用 `default` 主题和 `classic` 外观。
-- Mermaid 区块内容原样交给官方解析器,支持区块内部的 YAML `config` frontmatter。
-- 允许区块覆盖主题、外观、主题变量、布局及图表专属配置。
-- 固定 `strict` 安全级别、自动启动开关、文本和边数量限制、错误输出策略,并禁止区块注入原始 `themeCSS`。
-- 单个 Mermaid 图表解析或渲染失败时显示局部错误,其余图表继续渲染。
-- 增加 Mermaid 站点配置、单图错误隔离及区块 frontmatter 保留测试。
+- Mermaid、字体和图片完成后再启动分页。
+- 普通 Mermaid 保持主题原有尺寸和布局。
+- 根据纸张和页边距计算正文区域的 CSS 像素宽高。
+- 当 SVG 按正文宽度适配后仍高于一页时,读取 `viewBox` 并改为按正文高度等比例缩小。
+- 超高 Mermaid 整体换页、水平居中,不跨页裁切。
+- 浏览器实测 A4 超高图高度约 1000.56px,正文可用高度约 1001.57px,未侵入上下页边距。
+- 增加 SVG `viewBox` 解析、正文尺寸和缩放算法测试。
## 4. 已执行验证
-2026-07-25 在当前完整工作区成功执行:
+2026-07-26 在当前完整工作区成功执行:
```text
npm test
@@ -177,14 +183,17 @@ git diff --check
结果:
- 共享配置测试:6 项通过;
-- 渲染器测试:6 项通过;
-- 前端测试:10 项通过;
+- 渲染器测试:7 项通过;
+- 前端测试:21 项通过;
- 后端测试:6 项通过;
- 全项目类型检查通过;
- 生产构建通过;
- `git diff --check` 通过,仅出现工作区 LF 将来可能转为 CRLF 的提示;
- 导入器默认的已存在目标保护通过;
- GitHub、Pixyll 和 Whitey 三套本地主题的 CSS API 均返回成功,组合结果无残留 `@import`。
+- 同一份 W30 Markdown 在 GitHub 分页预览和 Typora PDF 中均为 4 页。
+- 三个本地主题无需刷新即可连续切换并重新分页。
+- 超高 Mermaid 整体缩放到单页正文区域,普通 Mermaid 尺寸不受影响。
已使用浏览器插件完成视觉验证:
@@ -213,7 +222,11 @@ git diff --check
## 5. 当前注意事项
- 工作区不是干净状态,禁止重置。
-- `apps/web/src/App.tsx` 是当前预览实现的主要文件。
+- `apps/web/src/App.tsx` 负责分页 iframe 生命周期和父页面消息处理。
+- `apps/web/src/paged-preview-frame.ts` 负责 iframe 内 Mermaid、资源等待和 Paged.js 分页。
+- `apps/web/src/paged-preview.ts` 负责分页协议、共享文档 CSS、页眉和页码 CSS。
+- `apps/web/src/mermaid-page-fit.ts` 负责超高 Mermaid 的单页等比例适配。
+- `apps/web/src/paged-table-handler.ts` 负责跨页表格表头处理。
- `apps/web/src/ExportSettingsDrawer.tsx` 是导出设置界面。
- `apps/web/src/export-settings.ts` 负责版本化浏览器缓存。
- `packages/core/src/export-config.ts` 是纸张、页边距、页眉页脚和页码的共享模型。
@@ -229,20 +242,13 @@ git diff --check
## 6. 推荐接手顺序
-### 阶段一:完成分页预览
+### 阶段一:提交分页预览
-导出设置、缓存和实时纸张几何已经完成。下一步按已确认设计引入 Paged.js,并实现:
-
-- 逐页预览;
-- 页眉左中右内容及变量替换;
-- 页脚页码、总页数、对齐和起始页码;
-- Mermaid 整体换页和超高图按页面正文高度等比例缩放;
-- 字体、图片和 Mermaid 完成后再分页;
-- 预览与未来 PDF 共用分页 HTML 和 CSS。
+当前分页预览已经实现并通过验证。检查完整差异并创建一个清晰的阶段提交后,再进入真实 PDF。
### 阶段二:实现真实 PDF
-分页预览完成并提交后再实现:
+分页预览提交后再实现:
1. 增加 Playwright 和固定版本 Chromium。
2. 抽取可复用的完整 HTML 文档组装器。
diff --git a/package-lock.json b/package-lock.json
index c8d6551..2c045f7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -41,6 +41,7 @@
"highlight.js": "^11.11.1",
"katex": "^0.16.28",
"mermaid": "^11.12.3",
+ "pagedjs": "^0.4.3",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
@@ -316,6 +317,26 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/polyfill": {
+ "version": "7.12.1",
+ "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.12.1.tgz",
+ "integrity": "sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g==",
+ "deprecated": "🚨 This package has been deprecated in favor of separate inclusion of a polyfill and regenerator-runtime (when needed). See the @babel/polyfill docs (https://babeljs.io/docs/en/babel-polyfill) for more information.",
+ "license": "MIT",
+ "dependencies": {
+ "core-js": "^2.6.5",
+ "regenerator-runtime": "^0.13.4"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/template": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
@@ -2266,6 +2287,12 @@
"node": ">=8"
}
},
+ "node_modules/clear-cut": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/clear-cut/-/clear-cut-2.0.2.tgz",
+ "integrity": "sha512-WVgn/gSejQ+0aoR8ucbKIdo6icduPZW6AbWwyUmAUgxy63rUYjwa5rj/HeoNPhf0/XPrl82X8bO/hwBkSmsFtg==",
+ "license": "MIT"
+ },
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -2355,6 +2382,14 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/core-js": {
+ "version": "2.6.12",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
+ "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==",
+ "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.",
+ "hasInstallScript": true,
+ "license": "MIT"
+ },
"node_modules/cose-base": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
@@ -2364,6 +2399,19 @@
"layout-base": "^1.0.0"
}
},
+ "node_modules/css-tree": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
+ "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.0.14",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -2420,6 +2468,19 @@
"integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==",
"license": "MIT"
},
+ "node_modules/d": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
+ "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
+ "license": "ISC",
+ "dependencies": {
+ "es5-ext": "^0.10.64",
+ "type": "^2.7.2"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
"node_modules/d3": {
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
@@ -3058,6 +3119,46 @@
"tests/types"
]
},
+ "node_modules/es5-ext": {
+ "version": "0.10.64",
+ "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
+ "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
+ "hasInstallScript": true,
+ "license": "ISC",
+ "dependencies": {
+ "es6-iterator": "^2.0.3",
+ "es6-symbol": "^3.1.3",
+ "esniff": "^2.0.1",
+ "next-tick": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/es6-iterator": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+ "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
+ "license": "MIT",
+ "dependencies": {
+ "d": "1",
+ "es5-ext": "^0.10.35",
+ "es6-symbol": "^3.1.1"
+ }
+ },
+ "node_modules/es6-symbol": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
+ "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
+ "license": "ISC",
+ "dependencies": {
+ "d": "^1.0.2",
+ "ext": "^1.7.0"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
"node_modules/esbuild": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
@@ -3122,6 +3223,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/esniff": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
+ "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
+ "license": "ISC",
+ "dependencies": {
+ "d": "^1.0.1",
+ "es5-ext": "^0.10.62",
+ "event-emitter": "^0.3.5",
+ "type": "^2.7.2"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
@@ -3145,6 +3261,16 @@
"@types/estree": "^1.0.0"
}
},
+ "node_modules/event-emitter": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
+ "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
+ "license": "MIT",
+ "dependencies": {
+ "d": "1",
+ "es5-ext": "~0.10.14"
+ }
+ },
"node_modules/expect-type": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
@@ -3155,6 +3281,15 @@
"node": ">=12.0.0"
}
},
+ "node_modules/ext": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
+ "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
+ "license": "ISC",
+ "dependencies": {
+ "type": "^2.7.2"
+ }
+ },
"node_modules/extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
@@ -3749,6 +3884,12 @@
"node": ">= 20"
}
},
+ "node_modules/mdn-data": {
+ "version": "2.0.14",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
+ "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
+ "license": "CC0-1.0"
+ },
"node_modules/mdurl": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz",
@@ -3825,6 +3966,12 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "node_modules/next-tick": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
+ "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
+ "license": "ISC"
+ },
"node_modules/node-releases": {
"version": "2.0.51",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
@@ -3864,6 +4011,19 @@
"integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==",
"license": "MIT"
},
+ "node_modules/pagedjs": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/pagedjs/-/pagedjs-0.4.3.tgz",
+ "integrity": "sha512-YtAN9JAjsQw1142gxEjEAwXvOF5nYQuDwnQ67RW2HZDkMLI+b4RsBE37lULZa9gAr6kDAOGBOhXI4wGMoY3raw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/polyfill": "^7.10.1",
+ "@babel/runtime": "^7.21.0",
+ "clear-cut": "^2.0.2",
+ "css-tree": "^1.1.3",
+ "event-emitter": "^0.3.5"
+ }
+ },
"node_modules/parse-srcset": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
@@ -4056,6 +4216,12 @@
"node": ">= 12.13.0"
}
},
+ "node_modules/regenerator-runtime": {
+ "version": "0.13.11",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
+ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
+ "license": "MIT"
+ },
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -4415,6 +4581,15 @@
"atomic-sleep": "^1.0.0"
}
},
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -4627,6 +4802,12 @@
"fsevents": "~2.3.3"
}
},
+ "node_modules/type": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
+ "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==",
+ "license": "ISC"
+ },
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",