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:
+154
-16
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user