feat: 实现真实分页预览
This commit is contained in:
@@ -14,6 +14,7 @@
|
|||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"katex": "^0.16.28",
|
"katex": "^0.16.28",
|
||||||
"mermaid": "^11.12.3",
|
"mermaid": "^11.12.3",
|
||||||
|
"pagedjs": "^0.4.3",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0"
|
"react-dom": "^19.2.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta
|
||||||
|
http-equiv="Content-Security-Policy"
|
||||||
|
content="default-src 'none'; script-src 'self'; style-src 'unsafe-inline'; font-src 'self' data:; img-src 'self' data:; connect-src 'self';"
|
||||||
|
/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>分页预览</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="preview-root" aria-live="polite"></div>
|
||||||
|
<script type="module" src="/src/paged-preview-frame.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+101
-212
@@ -8,19 +8,20 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
getPaperDimensionsMm,
|
getPaperDimensionsMm,
|
||||||
|
millimetersToCssPixels,
|
||||||
type ExportConfig
|
type ExportConfig
|
||||||
} from "@md-to-pdf/core";
|
} from "@md-to-pdf/core";
|
||||||
import highlightCss from "highlight.js/styles/github.css?inline";
|
|
||||||
import katexCss from "katex/dist/katex.min.css?inline";
|
|
||||||
import { ExportSettingsDrawer } from "./ExportSettingsDrawer";
|
import { ExportSettingsDrawer } from "./ExportSettingsDrawer";
|
||||||
import {
|
import {
|
||||||
loadExportConfig,
|
loadExportConfig,
|
||||||
resetExportConfig,
|
resetExportConfig,
|
||||||
saveExportConfig
|
saveExportConfig
|
||||||
} from "./export-settings";
|
} from "./export-settings";
|
||||||
import { createMermaidSiteConfig } from "./mermaid-config";
|
import {
|
||||||
import { renderMermaidDefinitions } from "./mermaid-renderer";
|
createPagedPreviewRenderRequest,
|
||||||
import { enablePrintMediaForPreview } from "./preview-styles";
|
isPagedPreviewFrameMessage,
|
||||||
|
type PagedPreviewPayload
|
||||||
|
} from "./paged-preview";
|
||||||
|
|
||||||
interface RenderedMarkdown {
|
interface RenderedMarkdown {
|
||||||
articleHtml: string;
|
articleHtml: string;
|
||||||
@@ -81,126 +82,6 @@ flowchart LR
|
|||||||
> 预览和 PDF 将共用同一份 HTML 与 CSS。
|
> 预览和 PDF 将共用同一份 HTML 与 CSS。
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const previewBaseCss = `
|
|
||||||
:root {
|
|
||||||
color-scheme: light;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
min-width: 0;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
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: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mermaid-error {
|
|
||||||
display: block;
|
|
||||||
padding: 0.85em 1em;
|
|
||||||
border: 1px solid #dc2626;
|
|
||||||
color: #991b1b;
|
|
||||||
background: #fef2f2;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const previewGeometryCss = `
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
width: 100% !important;
|
|
||||||
max-width: none !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#write {
|
|
||||||
width: 100% !important;
|
|
||||||
max-width: none !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
let mermaidPromise: Promise<(typeof import("mermaid"))["default"]> | undefined;
|
|
||||||
let mermaidRenderSequence = 0;
|
|
||||||
|
|
||||||
async function loadMermaid() {
|
|
||||||
if (!mermaidPromise) {
|
|
||||||
mermaidPromise = import("mermaid").then(({ default: mermaid }) => {
|
|
||||||
mermaid.initialize(createMermaidSiteConfig());
|
|
||||||
return mermaid;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return mermaidPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(value: string) {
|
|
||||||
return value
|
|
||||||
.replaceAll("&", "&")
|
|
||||||
.replaceAll("<", "<")
|
|
||||||
.replaceAll(">", ">")
|
|
||||||
.replaceAll('"', """);
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeStyleContent(value: string) {
|
|
||||||
return value.replace(/<\/style/gi, "<\\/style");
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildPreviewDocument(
|
|
||||||
result: RenderedMarkdown,
|
|
||||||
themeCss: string
|
|
||||||
) {
|
|
||||||
const previewThemeCss = enablePrintMediaForPreview(themeCss);
|
|
||||||
|
|
||||||
return `<!doctype html>
|
|
||||||
<html lang="${escapeHtml(result.metadata.language || "zh-CN")}">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta
|
|
||||||
http-equiv="Content-Security-Policy"
|
|
||||||
content="default-src 'none'; style-src 'unsafe-inline'; font-src 'self' data:; img-src 'self' data:;"
|
|
||||||
/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>${escapeHtml(result.metadata.title || "Markdown 预览")}</title>
|
|
||||||
<style>${escapeStyleContent(previewBaseCss)}</style>
|
|
||||||
<style>${escapeStyleContent(highlightCss)}</style>
|
|
||||||
<style>${escapeStyleContent(katexCss)}</style>
|
|
||||||
<style>${escapeStyleContent(previewThemeCss)}</style>
|
|
||||||
<style>${escapeStyleContent(previewGeometryCss)}</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
${result.articleHtml}
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const [markdown, setMarkdown] = useState(sampleMarkdown);
|
const [markdown, setMarkdown] = useState(sampleMarkdown);
|
||||||
const [fileName, setFileName] = useState("示例文档.md");
|
const [fileName, setFileName] = useState("示例文档.md");
|
||||||
@@ -214,9 +95,14 @@ export function App() {
|
|||||||
const [renderError, setRenderError] = useState("");
|
const [renderError, setRenderError] = useState("");
|
||||||
const [themeError, setThemeError] = useState("");
|
const [themeError, setThemeError] = useState("");
|
||||||
const [mermaidError, setMermaidError] = useState("");
|
const [mermaidError, setMermaidError] = useState("");
|
||||||
|
const [paginationError, setPaginationError] = useState("");
|
||||||
|
const [previewFrameReady, setPreviewFrameReady] = useState(false);
|
||||||
|
const [previewHeight, setPreviewHeight] = useState(500);
|
||||||
const previewFrameRef = useRef<HTMLIFrameElement>(null);
|
const previewFrameRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
const previewRequestIdRef = useRef(0);
|
||||||
|
|
||||||
const error = renderError || themeError || mermaidError;
|
const error =
|
||||||
|
renderError || themeError || mermaidError || paginationError;
|
||||||
const themeId = exportConfig.themeId;
|
const themeId = exportConfig.themeId;
|
||||||
const selectedTheme = themes.find((theme) => theme.id === themeId);
|
const selectedTheme = themes.find((theme) => theme.id === themeId);
|
||||||
const paperDimensions = getPaperDimensionsMm(
|
const paperDimensions = getPaperDimensionsMm(
|
||||||
@@ -226,16 +112,21 @@ export function App() {
|
|||||||
const paperStyle = {
|
const paperStyle = {
|
||||||
width: `${paperDimensions.width}mm`,
|
width: `${paperDimensions.width}mm`,
|
||||||
minWidth: `${paperDimensions.width}mm`,
|
minWidth: `${paperDimensions.width}mm`,
|
||||||
minHeight: `${paperDimensions.height}mm`,
|
minHeight: `${paperDimensions.height}mm`
|
||||||
paddingTop: exportConfig.paper.margins.top,
|
|
||||||
paddingRight: exportConfig.paper.margins.right,
|
|
||||||
paddingBottom: exportConfig.paper.margins.bottom,
|
|
||||||
paddingLeft: exportConfig.paper.margins.left
|
|
||||||
} satisfies CSSProperties;
|
} satisfies CSSProperties;
|
||||||
const previewDocument = useMemo(
|
const previewPayload = useMemo<PagedPreviewPayload | undefined>(
|
||||||
() =>
|
() =>
|
||||||
result && themeCss ? buildPreviewDocument(result, themeCss) : undefined,
|
result && themeCss
|
||||||
[result, themeCss]
|
? {
|
||||||
|
articleHtml: result.articleHtml,
|
||||||
|
fileName,
|
||||||
|
metadata: result.metadata,
|
||||||
|
features: result.features,
|
||||||
|
themeCss,
|
||||||
|
exportConfig
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
[exportConfig, fileName, result, themeCss]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -290,6 +181,7 @@ export function App() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
setPreviewFrameReady(false);
|
||||||
setThemeCss("");
|
setThemeCss("");
|
||||||
setThemeError("");
|
setThemeError("");
|
||||||
|
|
||||||
@@ -359,83 +251,80 @@ export function App() {
|
|||||||
};
|
};
|
||||||
}, [markdown]);
|
}, [markdown]);
|
||||||
|
|
||||||
async function handlePreviewLoad() {
|
useEffect(() => {
|
||||||
const frame = previewFrameRef.current;
|
function handlePreviewFrameMessage(event: MessageEvent<unknown>) {
|
||||||
const document = frame?.contentDocument;
|
const frame = previewFrameRef.current;
|
||||||
if (!frame || !document || !result) {
|
if (
|
||||||
|
event.origin !== window.location.origin ||
|
||||||
|
event.source !== frame?.contentWindow ||
|
||||||
|
!isPagedPreviewFrameMessage(event.data)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data.type === "ready") {
|
||||||
|
setPreviewFrameReady(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data.requestId !== previewRequestIdRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data.type === "rendering") {
|
||||||
|
setPaginationError("");
|
||||||
|
setMermaidError("");
|
||||||
|
setStatus("正在分页…");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data.type === "error") {
|
||||||
|
setPaginationError(event.data.message);
|
||||||
|
setStatus("分页失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPreviewHeight(
|
||||||
|
Math.max(
|
||||||
|
event.data.contentHeight,
|
||||||
|
millimetersToCssPixels(paperDimensions.height)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setMermaidError(
|
||||||
|
event.data.mermaidErrors.length > 0
|
||||||
|
? `Mermaid:${event.data.mermaidErrors.join(";")}`
|
||||||
|
: ""
|
||||||
|
);
|
||||||
|
setStatus(`预览已分页(${event.data.pageCount} 页)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("message", handlePreviewFrameMessage);
|
||||||
|
return () =>
|
||||||
|
window.removeEventListener("message", handlePreviewFrameMessage);
|
||||||
|
}, [paperDimensions.height]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const frameWindow = previewFrameRef.current?.contentWindow;
|
||||||
|
if (!frameWindow || !previewFrameReady || !previewPayload) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resizeFrame = () => {
|
previewRequestIdRef.current += 1;
|
||||||
frame.style.height = `${Math.max(
|
setPreviewHeight(
|
||||||
document.documentElement.scrollHeight,
|
Math.ceil(millimetersToCssPixels(paperDimensions.height))
|
||||||
document.body.scrollHeight,
|
);
|
||||||
500
|
frameWindow.postMessage(
|
||||||
)}px`;
|
createPagedPreviewRenderRequest(
|
||||||
};
|
previewRequestIdRef.current,
|
||||||
|
previewPayload
|
||||||
resizeFrame();
|
),
|
||||||
await document.fonts.ready;
|
window.location.origin
|
||||||
|
);
|
||||||
if (result.features.includes("mermaid")) {
|
}, [
|
||||||
const nodes = Array.from(
|
paperDimensions.height,
|
||||||
document.querySelectorAll<HTMLElement>(
|
previewFrameReady,
|
||||||
".mermaid[data-mermaid-pending]"
|
previewPayload
|
||||||
)
|
]);
|
||||||
);
|
|
||||||
|
|
||||||
if (nodes.length > 0) {
|
|
||||||
try {
|
|
||||||
const mermaid = await loadMermaid();
|
|
||||||
const outcomes = await renderMermaidDefinitions(
|
|
||||||
nodes.map((node) => node.textContent ?? ""),
|
|
||||||
async (definition) => {
|
|
||||||
mermaidRenderSequence += 1;
|
|
||||||
return mermaid.render(
|
|
||||||
`mermaid-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}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
setMermaidError(`Mermaid:${errors.join(";")}`);
|
|
||||||
}
|
|
||||||
} catch (reason: unknown) {
|
|
||||||
setMermaidError(
|
|
||||||
reason instanceof Error
|
|
||||||
? `Mermaid:${reason.message}`
|
|
||||||
: "Mermaid 渲染失败"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resizeFrame();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleFileChange(event: ChangeEvent<HTMLInputElement>) {
|
async function handleFileChange(event: ChangeEvent<HTMLInputElement>) {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
@@ -532,14 +421,14 @@ export function App() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="preview-scroll">
|
<div className="preview-scroll">
|
||||||
<div className="paper" style={paperStyle}>
|
<div className="paper" style={paperStyle}>
|
||||||
{previewDocument ? (
|
{previewPayload ? (
|
||||||
<iframe
|
<iframe
|
||||||
ref={previewFrameRef}
|
ref={previewFrameRef}
|
||||||
className="preview-frame"
|
className="preview-frame"
|
||||||
title="文档内容预览"
|
title="文档内容预览"
|
||||||
sandbox="allow-same-origin"
|
sandbox="allow-same-origin allow-scripts"
|
||||||
srcDoc={previewDocument}
|
src="/preview-frame.html"
|
||||||
onLoad={() => void handlePreviewLoad()}
|
style={{ height: `${previewHeight}px` }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="preview-loading">正在生成文档预览…</div>
|
<div className="preview-loading">正在生成文档预览…</div>
|
||||||
|
|||||||
@@ -470,7 +470,7 @@ export function ExportSettingsDrawer({
|
|||||||
<span>显示页脚分隔线</span>
|
<span>显示页脚分隔线</span>
|
||||||
</label>
|
</label>
|
||||||
<p className="setting-hint">
|
<p className="setting-hint">
|
||||||
页眉和页码将在下一阶段的分页预览中显示。
|
页眉和页码会实时显示在分页预览中。
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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<HTMLElement>(".mermaid")
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const diagram of diagrams) {
|
||||||
|
const svg = diagram.querySelector<SVGSVGElement>("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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<HTMLElement>("#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<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
|
||||||
|
) {
|
||||||
|
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<unknown>) => {
|
||||||
|
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"
|
||||||
|
});
|
||||||
@@ -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<PagedPreviewPayload, "fileName" | "metadata">
|
||||||
|
) {
|
||||||
|
const values: Record<string, string> = {
|
||||||
|
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<PagedPreviewPayload, "fileName" | "metadata">
|
||||||
|
) {
|
||||||
|
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<PagedPreviewPayload, "fileName" | "metadata">
|
||||||
|
) {
|
||||||
|
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<PagedPreviewRenderRequest>;
|
||||||
|
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<PagedPreviewFrameMessage>;
|
||||||
|
return (
|
||||||
|
candidate.scope === PAGED_PREVIEW_MESSAGE_SCOPE &&
|
||||||
|
["ready", "rendering", "rendered", "error"].includes(
|
||||||
|
candidate.type ?? ""
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<HTMLElement>(
|
||||||
|
`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<HTMLTableElement>(
|
||||||
|
`table[data-ref="${CSS.escape(ref)}"]`
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
!renderedTable ||
|
||||||
|
renderedTable.hasAttribute("data-repeated-header")
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceTable =
|
||||||
|
this.chunker.source.querySelector<HTMLTableElement>(
|
||||||
|
`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 };
|
||||||
Vendored
+41
@@ -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<string, string>;
|
||||||
|
|
||||||
|
export class Previewer {
|
||||||
|
chunker: {
|
||||||
|
destroy(): void;
|
||||||
|
};
|
||||||
|
polisher: {
|
||||||
|
destroy(): void;
|
||||||
|
};
|
||||||
|
preview(
|
||||||
|
content?: HTMLElement | DocumentFragment | string,
|
||||||
|
stylesheets?: PagedStylesheet[],
|
||||||
|
renderTo?: HTMLElement | string
|
||||||
|
): Promise<PagedFlow>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Handler {
|
||||||
|
constructor(
|
||||||
|
chunker: unknown,
|
||||||
|
polisher: unknown,
|
||||||
|
caller: unknown
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerHandlers(
|
||||||
|
...handlers: Array<typeof Handler>
|
||||||
|
): void;
|
||||||
|
}
|
||||||
+17
-6
@@ -47,7 +47,9 @@ button:disabled {
|
|||||||
.workspace {
|
.workspace {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 100vh;
|
height: 100vh;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar {
|
.topbar {
|
||||||
@@ -109,7 +111,7 @@ h1 {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: calc(100vh - 92px);
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-panel {
|
.editor-panel {
|
||||||
@@ -216,7 +218,7 @@ h1 {
|
|||||||
textarea {
|
textarea {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 560px;
|
min-height: 0;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
border: 0;
|
border: 0;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
@@ -249,8 +251,7 @@ textarea:focus {
|
|||||||
|
|
||||||
.paper {
|
.paper {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
background: #fff;
|
background: transparent;
|
||||||
box-shadow: 0 18px 48px rgb(37 49 43 / 16%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-frame {
|
.preview-frame {
|
||||||
@@ -258,7 +259,7 @@ textarea:focus {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: #fff;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-loading {
|
.preview-loading {
|
||||||
@@ -541,6 +542,12 @@ textarea:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
|
.workspace {
|
||||||
|
height: auto;
|
||||||
|
min-height: 100vh;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
.topbar {
|
.topbar {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
@@ -559,6 +566,10 @@ textarea:focus {
|
|||||||
min-height: 650px;
|
min-height: 650px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 560px;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-panel {
|
.editor-panel {
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
border-bottom: 1px solid #cbd2ce;
|
border-bottom: 1px solid #cbd2ce;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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: '<article id="write"><h1>分页测试</h1></article>',
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,8 +1,19 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
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: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|||||||
+56
-50
@@ -1,6 +1,6 @@
|
|||||||
# Markdown PDF 导出器进度
|
# Markdown PDF 导出器进度
|
||||||
|
|
||||||
最后更新:2026-07-25
|
最后更新:2026-07-26
|
||||||
|
|
||||||
## 1. 当前概况
|
## 1. 当前概况
|
||||||
|
|
||||||
@@ -19,13 +19,14 @@ main
|
|||||||
已有提交:
|
已有提交:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
52cf816 feat: 完善导出设置与打印预览
|
||||||
ec14f79 feat: 扩展本地主题兼容能力
|
ec14f79 feat: 扩展本地主题兼容能力
|
||||||
fa07472 feat: 实现网页实时预览
|
fa07472 feat: 实现网页实时预览
|
||||||
7224dfd feat: 实现 Markdown 渲染核心
|
7224dfd feat: 实现 Markdown 渲染核心
|
||||||
ec48bce chore: 初始化项目骨架
|
ec48bce chore: 初始化项目骨架
|
||||||
```
|
```
|
||||||
|
|
||||||
当前工作区存在未提交修改,主要是导出配置版本 2、导出设置抽屉、浏览器缓存和实时纸张尺寸及页边距预览。接手时必须保留并审查这些修改,不要重置工作区。
|
当前工作区存在未提交修改,主要是基于 Paged.js 的真实分页预览、页眉页码、跨页表格和超高 Mermaid 图表适配。接手时必须保留并审查这些修改,不要重置工作区。
|
||||||
|
|
||||||
## 2. 已完成
|
## 2. 已完成
|
||||||
|
|
||||||
@@ -119,53 +120,58 @@ ec48bce chore: 初始化项目骨架
|
|||||||
- 主题开发指南;
|
- 主题开发指南;
|
||||||
- 后端测试增至 6 项。
|
- 后端测试增至 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. 当前未提交工作
|
||||||
|
|
||||||
以下内容已写入工作区,但尚未提交:
|
以下内容已写入工作区,但尚未提交:
|
||||||
|
|
||||||
### 3.1 导出配置版本 2
|
### 3.1 Paged.js 真实分页预览
|
||||||
|
|
||||||
- 纸张只保留 A3、A4、A5、US-Letter 和 US-Legal。
|
- 引入 MIT 许可证的 Paged.js 0.4.3。
|
||||||
- 固定纸张尺寸分别为 297×420、210×297、148×210、216×279 和 216×356mm。
|
- 使用独立同源沙箱 iframe 执行分页,父页面通过严格消息协议传递渲染载荷。
|
||||||
- 默认 A4 纵向,四边页边距均为 16mm。
|
- 每张纸以真实物理尺寸独立显示,页面间保留 24px 间隔和预览阴影。
|
||||||
- 统一采用 96 CSS px/in、72 PDF pt/in 和 25.4mm/in 的物理单位换算。
|
- 主题 CSS 原样交给 Paged.js,使主题 `@media print` 规则与未来 PDF 一致。
|
||||||
- 保留横向和纵向。
|
- GitHub 主题打印字号已与 Typora PDF 对齐:正文 13px、一级标题 29.25px、二级标题 22.75px。
|
||||||
- 页脚模型收敛为页码设置,支持五种页码样式、对齐和起始页码。
|
- W30 对照文档在 Typora 和网页预览中均为 4 页。
|
||||||
- 校验页边距之和必须为正文保留正尺寸区域。
|
- 主题切换会重置 iframe 就绪握手,GitHub、Pixyll 和 Whitey 可连续切换且无需刷新。
|
||||||
- 增加 6 项共享配置测试。
|
|
||||||
|
|
||||||
### 3.2 导出设置和浏览器缓存
|
### 3.2 页眉、页码与跨页内容
|
||||||
|
|
||||||
- 增加响应式右侧导出设置抽屉。
|
- 页眉支持左中右三栏、标题、作者和文件名变量及分隔线。
|
||||||
- 支持五种纸张、方向和四边页边距。
|
- 页脚支持五种页码格式、左中右位置、起始页码和分隔线。
|
||||||
- 已提供页眉左中右内容、页脚页码样式、位置、起始页码和分隔线控件。
|
- 当前页码逐页递增,总页数使用真实分页结果。
|
||||||
- 配置和主题选择写入版本化 `localStorage`,不缓存 Markdown 正文。
|
- 标题避免孤立在页尾,段落使用孤行和寡行约束。
|
||||||
- 缓存缺失、损坏或版本过期时恢复默认配置。
|
- 长表格允许跨页,并通过 Paged.js 处理器重复表头。
|
||||||
- 切换到更小纸张时自动等比例收敛过大的旧页边距。
|
- 表格行、代码块、引用、公式、图片和 Mermaid 默认避免从中间断开。
|
||||||
- 增加 3 项浏览器缓存测试和 3 项打印媒体预览测试。
|
|
||||||
|
|
||||||
### 3.3 实时纸张预览
|
### 3.3 Mermaid 单页适配
|
||||||
|
|
||||||
- 纸张尺寸、方向和四边页边距实时作用于预览。
|
- Mermaid、字体和图片完成后再启动分页。
|
||||||
- 删除本地主题专用的 8mm 外边距特例。
|
- 普通 Mermaid 保持主题原有尺寸和布局。
|
||||||
- 在主题 CSS 后追加共享几何 CSS,统一清除 `body` 和 `#write` 的页面宽度、内外边距限制。
|
- 根据纸张和页边距计算正文区域的 CSS 像素宽高。
|
||||||
- 网页预览将主题的 `@media print` 规则按原始位置转换为屏幕预览规则,使 Typora GitHub 等主题使用与 PDF 相同的打印字号和行高。
|
- 当 SVG 按正文宽度适配后仍高于一页时,读取 `viewBox` 并改为按正文高度等比例缩小。
|
||||||
- 固定逻辑页面后续按 96 CSS px/in 分页,预览显示倍率只缩放外层,不参与正文换行和分页计算。
|
- 超高 Mermaid 整体换页、水平居中,不跨页裁切。
|
||||||
- 页眉和页码配置将在下一阶段分页预览中显示。
|
- 浏览器实测 A4 超高图高度约 1000.56px,正文可用高度约 1001.57px,未侵入上下页边距。
|
||||||
|
- 增加 SVG `viewBox` 解析、正文尺寸和缩放算法测试。
|
||||||
### 3.4 Mermaid 配置与错误隔离
|
|
||||||
|
|
||||||
- 使用 Mermaid 官方 npm 包,当前锁定安装版本为 11.16.0。
|
|
||||||
- 无区块配置时显式使用 `default` 主题和 `classic` 外观。
|
|
||||||
- Mermaid 区块内容原样交给官方解析器,支持区块内部的 YAML `config` frontmatter。
|
|
||||||
- 允许区块覆盖主题、外观、主题变量、布局及图表专属配置。
|
|
||||||
- 固定 `strict` 安全级别、自动启动开关、文本和边数量限制、错误输出策略,并禁止区块注入原始 `themeCSS`。
|
|
||||||
- 单个 Mermaid 图表解析或渲染失败时显示局部错误,其余图表继续渲染。
|
|
||||||
- 增加 Mermaid 站点配置、单图错误隔离及区块 frontmatter 保留测试。
|
|
||||||
|
|
||||||
## 4. 已执行验证
|
## 4. 已执行验证
|
||||||
|
|
||||||
2026-07-25 在当前完整工作区成功执行:
|
2026-07-26 在当前完整工作区成功执行:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
npm test
|
npm test
|
||||||
@@ -177,14 +183,17 @@ git diff --check
|
|||||||
结果:
|
结果:
|
||||||
|
|
||||||
- 共享配置测试:6 项通过;
|
- 共享配置测试:6 项通过;
|
||||||
- 渲染器测试:6 项通过;
|
- 渲染器测试:7 项通过;
|
||||||
- 前端测试:10 项通过;
|
- 前端测试:21 项通过;
|
||||||
- 后端测试:6 项通过;
|
- 后端测试:6 项通过;
|
||||||
- 全项目类型检查通过;
|
- 全项目类型检查通过;
|
||||||
- 生产构建通过;
|
- 生产构建通过;
|
||||||
- `git diff --check` 通过,仅出现工作区 LF 将来可能转为 CRLF 的提示;
|
- `git diff --check` 通过,仅出现工作区 LF 将来可能转为 CRLF 的提示;
|
||||||
- 导入器默认的已存在目标保护通过;
|
- 导入器默认的已存在目标保护通过;
|
||||||
- GitHub、Pixyll 和 Whitey 三套本地主题的 CSS API 均返回成功,组合结果无残留 `@import`。
|
- GitHub、Pixyll 和 Whitey 三套本地主题的 CSS API 均返回成功,组合结果无残留 `@import`。
|
||||||
|
- 同一份 W30 Markdown 在 GitHub 分页预览和 Typora PDF 中均为 4 页。
|
||||||
|
- 三个本地主题无需刷新即可连续切换并重新分页。
|
||||||
|
- 超高 Mermaid 整体缩放到单页正文区域,普通 Mermaid 尺寸不受影响。
|
||||||
|
|
||||||
已使用浏览器插件完成视觉验证:
|
已使用浏览器插件完成视觉验证:
|
||||||
|
|
||||||
@@ -213,7 +222,11 @@ git diff --check
|
|||||||
## 5. 当前注意事项
|
## 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/ExportSettingsDrawer.tsx` 是导出设置界面。
|
||||||
- `apps/web/src/export-settings.ts` 负责版本化浏览器缓存。
|
- `apps/web/src/export-settings.ts` 负责版本化浏览器缓存。
|
||||||
- `packages/core/src/export-config.ts` 是纸张、页边距、页眉页脚和页码的共享模型。
|
- `packages/core/src/export-config.ts` 是纸张、页边距、页眉页脚和页码的共享模型。
|
||||||
@@ -229,20 +242,13 @@ git diff --check
|
|||||||
|
|
||||||
## 6. 推荐接手顺序
|
## 6. 推荐接手顺序
|
||||||
|
|
||||||
### 阶段一:完成分页预览
|
### 阶段一:提交分页预览
|
||||||
|
|
||||||
导出设置、缓存和实时纸张几何已经完成。下一步按已确认设计引入 Paged.js,并实现:
|
当前分页预览已经实现并通过验证。检查完整差异并创建一个清晰的阶段提交后,再进入真实 PDF。
|
||||||
|
|
||||||
- 逐页预览;
|
|
||||||
- 页眉左中右内容及变量替换;
|
|
||||||
- 页脚页码、总页数、对齐和起始页码;
|
|
||||||
- Mermaid 整体换页和超高图按页面正文高度等比例缩放;
|
|
||||||
- 字体、图片和 Mermaid 完成后再分页;
|
|
||||||
- 预览与未来 PDF 共用分页 HTML 和 CSS。
|
|
||||||
|
|
||||||
### 阶段二:实现真实 PDF
|
### 阶段二:实现真实 PDF
|
||||||
|
|
||||||
分页预览完成并提交后再实现:
|
分页预览提交后再实现:
|
||||||
|
|
||||||
1. 增加 Playwright 和固定版本 Chromium。
|
1. 增加 Playwright 和固定版本 Chromium。
|
||||||
2. 抽取可复用的完整 HTML 文档组装器。
|
2. 抽取可复用的完整 HTML 文档组装器。
|
||||||
|
|||||||
Generated
+181
@@ -41,6 +41,7 @@
|
|||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"katex": "^0.16.28",
|
"katex": "^0.16.28",
|
||||||
"mermaid": "^11.12.3",
|
"mermaid": "^11.12.3",
|
||||||
|
"pagedjs": "^0.4.3",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0"
|
"react-dom": "^19.2.0"
|
||||||
},
|
},
|
||||||
@@ -316,6 +317,26 @@
|
|||||||
"@babel/core": "^7.0.0-0"
|
"@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": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.29.7",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||||
@@ -2266,6 +2287,12 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/cliui": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
@@ -2355,6 +2382,14 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"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": {
|
"node_modules/cose-base": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
|
||||||
@@ -2364,6 +2399,19 @@
|
|||||||
"layout-base": "^1.0.0"
|
"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": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
@@ -2420,6 +2468,19 @@
|
|||||||
"integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==",
|
"integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/d3": {
|
||||||
"version": "7.9.0",
|
"version": "7.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
|
||||||
@@ -3058,6 +3119,46 @@
|
|||||||
"tests/types"
|
"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": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
||||||
@@ -3122,6 +3223,21 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/esprima": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||||
@@ -3145,6 +3261,16 @@
|
|||||||
"@types/estree": "^1.0.0"
|
"@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": {
|
"node_modules/expect-type": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
|
||||||
@@ -3155,6 +3281,15 @@
|
|||||||
"node": ">=12.0.0"
|
"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": {
|
"node_modules/extend-shallow": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||||
@@ -3749,6 +3884,12 @@
|
|||||||
"node": ">= 20"
|
"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": {
|
"node_modules/mdurl": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz",
|
"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": "^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": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.51",
|
"version": "2.0.51",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
|
||||||
@@ -3864,6 +4011,19 @@
|
|||||||
"integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==",
|
"integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/parse-srcset": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
|
||||||
@@ -4056,6 +4216,12 @@
|
|||||||
"node": ">= 12.13.0"
|
"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": {
|
"node_modules/require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
@@ -4415,6 +4581,15 @@
|
|||||||
"atomic-sleep": "^1.0.0"
|
"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": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -4627,6 +4802,12 @@
|
|||||||
"fsevents": "~2.3.3"
|
"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": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
|||||||
Reference in New Issue
Block a user