135 lines
3.1 KiB
TypeScript
135 lines
3.1 KiB
TypeScript
import type { ExportConfig } from "@md-to-pdf/core";
|
|
|
|
export interface PdfExportRequest {
|
|
markdown: string;
|
|
fileName: string;
|
|
language: string;
|
|
exportConfig: ExportConfig;
|
|
}
|
|
|
|
export interface PdfExportResult {
|
|
blob: Blob;
|
|
fileName: string;
|
|
pageCount: number | undefined;
|
|
mermaidErrorCount: number;
|
|
}
|
|
|
|
export interface CachedPdfExport {
|
|
key: string;
|
|
result: PdfExportResult;
|
|
}
|
|
|
|
export function createPdfExportCacheKey(request: PdfExportRequest) {
|
|
return JSON.stringify([
|
|
request.markdown,
|
|
request.fileName,
|
|
request.language,
|
|
request.exportConfig
|
|
]);
|
|
}
|
|
|
|
export function getCachedPdfExport(
|
|
cache: CachedPdfExport | undefined,
|
|
key: string
|
|
) {
|
|
return cache?.key === key ? cache.result : undefined;
|
|
}
|
|
|
|
export async function getOrRequestPdfExport(
|
|
request: PdfExportRequest,
|
|
cache: CachedPdfExport | undefined,
|
|
requester: (
|
|
request: PdfExportRequest
|
|
) => Promise<PdfExportResult> = requestPdfExport
|
|
): Promise<CachedPdfExport> {
|
|
const key = createPdfExportCacheKey(request);
|
|
const cached = getCachedPdfExport(cache, key);
|
|
return {
|
|
key,
|
|
result: cached ?? (await requester(request))
|
|
};
|
|
}
|
|
|
|
function parseNumericHeader(value: string | null) {
|
|
if (value === null) {
|
|
return undefined;
|
|
}
|
|
const parsed = Number.parseInt(value, 10);
|
|
return Number.isInteger(parsed) && parsed >= 0 ? parsed : undefined;
|
|
}
|
|
|
|
export function parseContentDispositionFileName(
|
|
contentDisposition: string | null
|
|
) {
|
|
const encoded = contentDisposition?.match(
|
|
/filename\*=UTF-8''([^;]+)/i
|
|
)?.[1];
|
|
if (encoded) {
|
|
try {
|
|
return decodeURIComponent(encoded);
|
|
} catch {
|
|
// 使用兼容文件名回退。
|
|
}
|
|
}
|
|
|
|
return (
|
|
contentDisposition
|
|
?.match(/filename="([^"]+)"/i)?.[1]
|
|
?.trim() || "document.pdf"
|
|
);
|
|
}
|
|
|
|
async function readErrorMessage(response: Response) {
|
|
try {
|
|
const payload = (await response.json()) as {
|
|
message?: unknown;
|
|
};
|
|
if (typeof payload.message === "string") {
|
|
return payload.message;
|
|
}
|
|
} catch {
|
|
// 非 JSON 错误响应使用通用提示。
|
|
}
|
|
return "PDF 导出失败";
|
|
}
|
|
|
|
export async function requestPdfExport(
|
|
request: PdfExportRequest,
|
|
fetcher: typeof fetch = fetch
|
|
): Promise<PdfExportResult> {
|
|
const response = await fetcher("/api/pdf", {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json"
|
|
},
|
|
body: JSON.stringify(request)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(await readErrorMessage(response));
|
|
}
|
|
|
|
return {
|
|
blob: await response.blob(),
|
|
fileName: parseContentDispositionFileName(
|
|
response.headers.get("content-disposition")
|
|
),
|
|
pageCount: parseNumericHeader(
|
|
response.headers.get("x-pdf-page-count")
|
|
),
|
|
mermaidErrorCount:
|
|
parseNumericHeader(
|
|
response.headers.get("x-mermaid-error-count")
|
|
) ?? 0
|
|
};
|
|
}
|
|
|
|
export function downloadPdf(blob: Blob, fileName: string) {
|
|
const url = URL.createObjectURL(blob);
|
|
const anchor = document.createElement("a");
|
|
anchor.href = url;
|
|
anchor.download = fileName;
|
|
anchor.click();
|
|
window.setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
}
|