release: 发布 v0.5.0

新增共享 Preview Engine,统一 Web 连续预览、快速分页、Playwright PDF 与 Electron PDF;实现稳定前缀复用和修改位置后的增量分页,保留媒体块按文档顺序串行回填与单次重排。

完善跨端链接与桌面文档工作流:Web 受控处理锚点和 HTTP/HTTPS 外链;Desktop 支持本地路径、file URI、系统协议、多窗口、同文件单例、Markdown 当前或新窗口打开,以及聚焦时外部文件变化提示。

统一四套内置主题名称并默认使用 Typora Github;修复连续预览双滚动条、ECharts 尺寸、PDF 本地链接、围栏代码块 Typora DOM 与重复行内样式;桌面发行链强制完整重建内嵌 Web,避免安装包携带陈旧资源。

发布 Web/Compose 与 Windows NSIS/ZIP:镜像 yixiong/md-to-pdf:v0.5.0 已健康部署;NSIS SHA-256 为 60992D1FDCA513F46346C78478537EB4159D8C0E76B41ECF3CDC25BE77707D92,ZIP SHA-256 为 D74F82293FB67126E583546CBA894569EFC9A0B1B6343FAC648CCC95F1D188D8,本机安装版已升级至 v0.5.0。

验证:全项目 238 项测试通过,类型检查、生产构建和 git diff --check 通过;Web 快速/连续/精确预览、Compose、Desktop 多窗口、窗口状态、文件关联、链接与代码块均完成真实环境验收。
This commit is contained in:
SkyJourney
2026-07-28 18:01:22 +08:00
parent 925b0d1485
commit 58087d0c7e
86 changed files with 4564 additions and 1204 deletions
+154 -16
View File
@@ -27,7 +27,7 @@ import {
createPagedPreviewRenderRequest,
isPagedPreviewFrameMessage,
type PagedPreviewPayload
} from "./paged-preview";
} from "@md-to-pdf/preview-engine";
import {
getNearestPageNumber,
getPreviewPageScrollTop
@@ -53,6 +53,7 @@ import {
downloadMarkdown,
ensureMarkdownFileName
} from "./document-file";
import { resolveWebDocumentLinkAction } from "./document-link";
import {
type ThemeSummary,
useThemeResources
@@ -64,7 +65,7 @@ const PrecisePdfPreview = lazy(async () => {
return { default: module.PrecisePdfPreview };
});
type PreviewMode = "quick" | "precise";
type PreviewMode = "quick" | "precise" | "continuous";
type DocumentKind = "sample" | "new" | "file";
const noImageResources: never[] = [];
@@ -121,8 +122,11 @@ export function App() {
return;
}
let active = true;
const consumeOpenedMarkdown = async () => {
const consumeOpenedMarkdown = async (
reason: "open" | "reload" | "replace" = "open"
) => {
if (
reason === "open" &&
documentDirtyRef.current &&
!window.confirm("当前文档有未保存修改,是否放弃并打开其他文件?")
) {
@@ -160,8 +164,8 @@ export function App() {
}
}
};
const removeListener = bridge.onMarkdownOpened(() => {
void consumeOpenedMarkdown();
const removeListener = bridge.onMarkdownOpened((reason) => {
void consumeOpenedMarkdown(reason);
});
void consumeOpenedMarkdown();
return () => {
@@ -170,6 +174,18 @@ export function App() {
};
}, []);
useEffect(() => {
const bridge = window.mdToPdfDesktop;
if (!bridge) {
return;
}
void bridge.setDocumentDirty(documentDirty).catch(
(reason: unknown) => {
console.warn("无法同步桌面文档修改状态", reason);
}
);
}, [documentDirty]);
useEffect(() => {
if (!moreMenuOpen) {
return;
@@ -359,6 +375,11 @@ export function App() {
return;
}
if (event.data.type === "link") {
handlePagedPreviewLink(event.data.href);
return;
}
if (event.data.requestId !== previewRequestIdRef.current) {
return;
}
@@ -369,7 +390,11 @@ export function App() {
setMermaidError("");
setQuickPreviewContentHeight(undefined);
setPreviewPagination({ current: 0, total: 0 });
setStatus("正在分页…");
setStatus(
event.data.layout === "continuous"
? "正在更新连续预览…"
: "正在分页…"
);
return;
}
@@ -389,12 +414,20 @@ export function App() {
? `Mermaid${event.data.mermaidErrors.join("")}`
: ""
);
setStatus(`预览已分页(${event.data.pageCount} 页)`);
setStatus(
event.data.layout === "continuous"
? "连续预览已更新"
: `预览已分页(${event.data.pageCount} 页)`
);
setQuickPreviewContentHeight(event.data.contentHeight);
setPreviewPagination({
current: event.data.pageCount > 0 ? 1 : 0,
total: event.data.pageCount
});
setPreviewPagination(
event.data.layout === "continuous"
? { current: 0, total: 0 }
: {
current: event.data.pageCount > 0 ? 1 : 0,
total: event.data.pageCount
}
);
window.requestAnimationFrame(() => {
synchronizeScroll("editor");
window.requestAnimationFrame(updatePreviewCurrentPage);
@@ -435,11 +468,14 @@ export function App() {
frameWindow.postMessage(
createPagedPreviewRenderRequest(
previewRequestIdRef.current,
previewPayload
previewPayload,
previewMode === "continuous"
? "continuous"
: "paged"
),
window.location.origin
);
}, [previewFrameReady, previewPayload]);
}, [previewFrameReady, previewMode, previewPayload]);
function getOuterPreviewScroller() {
const previewScroller = previewScrollRef.current;
@@ -456,6 +492,79 @@ export function App() {
return getOuterPreviewScroller();
}
function handlePagedPreviewLink(href: string) {
const action = resolveWebDocumentLinkAction(href);
if (action.type !== "anchor" && window.mdToPdfDesktop) {
void window.mdToPdfDesktop.openDocumentLink(href).catch(
(reason: unknown) => {
setAppError(
reason instanceof Error
? reason.message
: "无法打开文档链接"
);
}
);
return;
}
if (action.type === "open") {
window.open(
action.href,
"_blank",
"noopener,noreferrer"
);
return;
}
if (action.type !== "anchor") {
return;
}
const frame = previewFrameRef.current;
const frameDocument = frame?.contentDocument;
const scrollElement = getPreviewScroller();
if (!frame || !frameDocument || !scrollElement) {
return;
}
const rawAnchorId = action.href.slice(1);
const anchorIds = [rawAnchorId];
try {
const decodedAnchorId = decodeURIComponent(rawAnchorId);
if (decodedAnchorId !== rawAnchorId) {
anchorIds.push(decodedAnchorId);
}
} catch {
// 保留原始片段继续查找,兼容不完整的百分号编码。
}
const target = anchorIds
.map(
(anchorId) =>
frameDocument.getElementById(anchorId) ??
frameDocument.getElementsByName(anchorId).item(0)
)
.find((element) => Boolean(element));
if (!target) {
return;
}
const scrollerViewportTop =
scrollElement === document.scrollingElement
? 0
: scrollElement.getBoundingClientRect().top;
const frameRect = frame.getBoundingClientRect();
const frameScale =
frame.offsetWidth > 0
? frameRect.width / frame.offsetWidth
: 1;
const targetTop = getPreviewPageScrollTop(
scrollElement.scrollTop,
frameRect.top +
target.getBoundingClientRect().top * frameScale,
scrollerViewportTop
);
scrollElement.scrollTo({ top: Math.max(0, targetTop) });
window.requestAnimationFrame(updatePreviewCurrentPage);
}
function synchronizeScroll(origin: "editor" | "preview") {
const source =
origin === "editor" ? editorRef.current : getPreviewScroller();
@@ -487,6 +596,9 @@ export function App() {
updatePrecisePreviewCurrentPage();
return;
}
if (previewModeRef.current === "continuous") {
return;
}
const frame = previewFrameRef.current;
const frameDocument = frame?.contentDocument;
const scrollElement = getPreviewScroller();
@@ -840,8 +952,12 @@ export function App() {
if (mode === previewMode) {
return;
}
const keepsFrame =
previewMode !== "precise" && mode !== "precise";
setPreviewMode(mode);
setPreviewFrameReady(false);
if (!keepsFrame) {
setPreviewFrameReady(false);
}
setQuickPreviewContentHeight(undefined);
setPreviewPagination({ current: 0, total: 0 });
previewScrollRef.current?.scrollTo({ top: 0 });
@@ -1041,6 +1157,18 @@ export function App() {
>
</button>
<button
type="button"
className={
previewMode === "continuous" ? "is-active" : ""
}
aria-pressed={previewMode === "continuous"}
onClick={() =>
handlePreviewModeChange("continuous")
}
>
</button>
</span>
<label className="theme-control">
<span className="sr-only"></span>
@@ -1090,12 +1218,20 @@ export function App() {
className="preview-scroll"
onScroll={handlePreviewScroll}
>
{previewMode === "quick" ? (
<div className="paper" style={paperStyle}>
{previewMode !== "precise" ? (
<div
className={`paper${
previewMode === "continuous"
? " continuous-paper"
: ""
}`}
style={paperStyle}
>
{previewPayload ? (
<iframe
ref={previewFrameRef}
className="preview-frame"
scrolling="no"
style={previewFrameStyle}
title="文档内容预览"
sandbox="allow-same-origin allow-scripts"
@@ -1122,6 +1258,8 @@ export function App() {
<PrecisePdfPreview
blob={cachedPdf.blob}
zoomPercent={previewZoom}
onExternalLink={handlePagedPreviewLink}
onNavigateToPage={handlePreviewPageChange}
onPageCount={handlePrecisePageCount}
/>
</Suspense>