feat: 实现 Chromium PDF 导出与精确预览
This commit is contained in:
@@ -1,20 +1,12 @@
|
||||
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 { PagedDocumentRuntime } from "./paged-document-runtime";
|
||||
import {
|
||||
buildPagedMediaCss,
|
||||
documentBaseCss,
|
||||
documentGeometryCss,
|
||||
formatPageNumber,
|
||||
isPagedPreviewRenderRequest,
|
||||
PAGED_PREVIEW_MESSAGE_SCOPE,
|
||||
type PagedPreviewFrameMessage,
|
||||
type PagedPreviewPayload
|
||||
} from "./paged-preview";
|
||||
import { resolvePagedRenderTarget } from "./paged-render-target";
|
||||
import { resolveMermaidOutputMode } from "./mermaid-static-image";
|
||||
|
||||
function getPreviewRoot() {
|
||||
const element = document.querySelector<HTMLElement>("#preview-root");
|
||||
@@ -25,151 +17,18 @@ function getPreviewRoot() {
|
||||
}
|
||||
|
||||
const previewRoot = getPreviewRoot();
|
||||
document.documentElement.dataset.renderTarget = "preview";
|
||||
const renderTarget = resolvePagedRenderTarget(window.location.search);
|
||||
const mermaidOutput = resolveMermaidOutputMode(window.location.search);
|
||||
const runtime = new PagedDocumentRuntime(previewRoot);
|
||||
document.documentElement.dataset.renderTarget = renderTarget;
|
||||
|
||||
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<HTMLElement>(
|
||||
".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<void>((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<HTMLElement>(".pagedjs_page")
|
||||
);
|
||||
|
||||
for (const [pageIndex, page] of pages.entries()) {
|
||||
const content = page.querySelector<HTMLElement>(
|
||||
`.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
|
||||
@@ -184,65 +43,38 @@ async function renderPagedPreview(
|
||||
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) {
|
||||
const result = await runtime.render(payload, {
|
||||
target: renderTarget,
|
||||
mermaidOutput,
|
||||
shouldContinue: () => requestId === latestRequestId
|
||||
});
|
||||
if (!result) {
|
||||
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
|
||||
...result
|
||||
});
|
||||
}
|
||||
|
||||
window.__mdToPdfRender = async (
|
||||
payload,
|
||||
target = renderTarget
|
||||
) => {
|
||||
const result = await runtime.render(payload, {
|
||||
target,
|
||||
mermaidOutput
|
||||
});
|
||||
if (!result) {
|
||||
throw new Error("分页文档渲染已取消");
|
||||
}
|
||||
return result;
|
||||
};
|
||||
document.documentElement.dataset.runtimeReady = "true";
|
||||
|
||||
window.addEventListener("message", (event: MessageEvent<unknown>) => {
|
||||
if (
|
||||
event.origin !== window.location.origin ||
|
||||
|
||||
Reference in New Issue
Block a user