release: 发布 v0.6.1 DOCX 视觉一致性修复
新增通用 CSS 到 OOXML 翻译修复,统一字体、字距、精确行距、段落、列表、表格、引用、代码块与行内代码连续性,不引入按主题 ID 分支。 新增 MdTP Mono 并统一 Serif、Sans、Mono 三字体包的 Chromium 与 DOCX 使用链;字体声明、嵌入部件和 Word/WPS 实际采用均进入硬门禁。 重建封面整页及正文语义块视觉差分,14 套主题、纵横两个方向、五组页边距共 140 个真实场景全部通过,阻断失败和诊断失败均为零。 源码服务、Docker Web API 与实际安装 Desktop 的 red-briefing 导出均包含 5 个字体部件;Word/WPS 原生渲染和逐页复核通过。修复 Docker 构建上下文与运行层复用软链接,并完善 v0.6.1 版本、发行说明和发布归集。 验证:npm test(116 个文件、616 项测试)、npm run typecheck、npm run build、git diff --check 全部通过。Desktop 安装器与 ZIP、Docker v0.6.1 镜像已生成;Windows 产物仍为未签名内部发行。
This commit is contained in:
@@ -57,8 +57,8 @@ npm run verify:pandoc-runtime -w @md-to-pdf/desktop
|
||||
```
|
||||
|
||||
版本化目录包、NSIS `Setup.exe` 和 ZIP 输出到
|
||||
`apps/desktop/out/v0.6.0/`。该目录被 Git 忽略。公司内部分发以
|
||||
`MorphDoc-0.6.0-x86_64-Setup.exe` 为正式安装包,ZIP 作为免安装
|
||||
`apps/desktop/out/v0.6.1/`。该目录被 Git 忽略。公司内部分发以
|
||||
`MorphDoc-0.6.1-x86_64-Setup.exe` 为正式安装包,ZIP 作为免安装
|
||||
辅助包;当前未配置代码签名,Windows 首次运行可能显示“未知发布者”
|
||||
提示。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@md-to-pdf/desktop",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"private": true,
|
||||
"productName": "MorphDoc",
|
||||
"description": "墨呈桌面端:面向结构化 Markdown 的主题化文档创作与发布工具",
|
||||
|
||||
@@ -72,13 +72,6 @@ export async function captureDocxMediaWithElectronWebContents(
|
||||
signal?: AbortSignal,
|
||||
timeouts: ElectronDocxMediaCaptureTimeouts = DEFAULT_CAPTURE_TIMEOUTS
|
||||
) {
|
||||
signal?.throwIfAborted();
|
||||
const plan = (await withTimeout(
|
||||
webContents.executeJavaScript(createRenderScript(request), true),
|
||||
timeouts.renderMs,
|
||||
`桌面 DOCX 媒体布局超过 ${timeouts.renderMs}ms`,
|
||||
signal
|
||||
)) as DocxMediaCapturePlan;
|
||||
signal?.throwIfAborted();
|
||||
const ownsDebugger = !webContents.debugger.isAttached();
|
||||
if (ownsDebugger) {
|
||||
@@ -86,6 +79,16 @@ export async function captureDocxMediaWithElectronWebContents(
|
||||
}
|
||||
|
||||
try {
|
||||
await webContents.debugger.sendCommand("Emulation.setEmulatedMedia", {
|
||||
media: "print"
|
||||
});
|
||||
const plan = (await withTimeout(
|
||||
webContents.executeJavaScript(createRenderScript(request), true),
|
||||
timeouts.renderMs,
|
||||
`桌面 DOCX 媒体布局超过 ${timeouts.renderMs}ms`,
|
||||
signal
|
||||
)) as DocxMediaCapturePlan;
|
||||
signal?.throwIfAborted();
|
||||
const captures = [];
|
||||
for (const target of plan.targets) {
|
||||
signal?.throwIfAborted();
|
||||
@@ -117,8 +120,13 @@ export async function captureDocxMediaWithElectronWebContents(
|
||||
signal?.throwIfAborted();
|
||||
return { plan, captures };
|
||||
} finally {
|
||||
if (ownsDebugger && webContents.debugger.isAttached()) {
|
||||
webContents.debugger.detach();
|
||||
if (webContents.debugger.isAttached()) {
|
||||
await webContents.debugger
|
||||
.sendCommand("Emulation.setEmulatedMedia", { media: "screen" })
|
||||
.catch(() => undefined);
|
||||
if (ownsDebugger && webContents.debugger.isAttached()) {
|
||||
webContents.debugger.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,14 @@ describe("Electron DOCX 媒体捕获", () => {
|
||||
png: Buffer.from("png")
|
||||
});
|
||||
expect(attach).toHaveBeenCalledWith("1.3");
|
||||
expect(sendCommand).toHaveBeenCalledWith(
|
||||
"Emulation.setEmulatedMedia",
|
||||
{ media: "print" }
|
||||
);
|
||||
expect(sendCommand).toHaveBeenCalledWith(
|
||||
"Emulation.setEmulatedMedia",
|
||||
{ media: "screen" }
|
||||
);
|
||||
expect(sendCommand).toHaveBeenCalledWith(
|
||||
"Page.captureScreenshot",
|
||||
expect.objectContaining({
|
||||
@@ -106,10 +114,18 @@ describe("Electron DOCX 媒体捕获", () => {
|
||||
});
|
||||
|
||||
it("媒体布局永久不返回时报告局部超时", async () => {
|
||||
let attached = false;
|
||||
const webContents = {
|
||||
executeJavaScript: vi.fn(() => new Promise(() => undefined)),
|
||||
debugger: {
|
||||
isAttached: vi.fn(() => false)
|
||||
isAttached: vi.fn(() => attached),
|
||||
attach: vi.fn(() => {
|
||||
attached = true;
|
||||
}),
|
||||
detach: vi.fn(() => {
|
||||
attached = false;
|
||||
}),
|
||||
sendCommand: vi.fn(async () => undefined)
|
||||
}
|
||||
} as unknown as WebContents;
|
||||
|
||||
@@ -136,7 +152,11 @@ describe("Electron DOCX 媒体捕获", () => {
|
||||
attached = true;
|
||||
}),
|
||||
detach,
|
||||
sendCommand: vi.fn(() => new Promise(() => undefined))
|
||||
sendCommand: vi.fn((method: string) =>
|
||||
method === "Page.captureScreenshot"
|
||||
? new Promise(() => undefined)
|
||||
: Promise.resolve(undefined)
|
||||
)
|
||||
}
|
||||
} as unknown as WebContents;
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ describe("桌面发行构建链", () => {
|
||||
artifactPrefix: "MorphDoc"
|
||||
});
|
||||
expect(desktopManifest).toMatchObject({
|
||||
version: "0.6.0",
|
||||
version: "0.6.1",
|
||||
productName: "MorphDoc"
|
||||
});
|
||||
expect(builderConfig).toContain("appId: brand.appId");
|
||||
|
||||
@@ -11,6 +11,9 @@ import { createPdfGenerator } from "../dist/pdf-engine.js";
|
||||
|
||||
const directory = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repositoryDirectory = path.resolve(directory, "../../..");
|
||||
const appVersion = JSON.parse(
|
||||
fs.readFileSync(path.join(repositoryDirectory, "package.json"), "utf8")
|
||||
).version;
|
||||
const themesDirectory = path.join(repositoryDirectory, "themes");
|
||||
const samplesDirectory = path.join(repositoryDirectory, "samples", "themes");
|
||||
const webDirectory = path.join(repositoryDirectory, "apps", "web", "dist");
|
||||
@@ -32,6 +35,37 @@ const fontPackRoot = path.resolve(
|
||||
);
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
const inlineCodeContinuityProbes = [
|
||||
{
|
||||
id: "paragraph",
|
||||
code: "mdtp_ic_p",
|
||||
text: "段落前mdtp_ic_p段落后"
|
||||
},
|
||||
{
|
||||
id: "list-item",
|
||||
code: "mdtp_ic_l",
|
||||
text: "列表前mdtp_ic_l列表后"
|
||||
},
|
||||
{
|
||||
id: "table-cell",
|
||||
code: "mdtp_ic_c",
|
||||
text: "单元格前mdtp_ic_c单元格后"
|
||||
}
|
||||
];
|
||||
|
||||
const inlineCodeContinuityMarkdown = `
|
||||
|
||||
## 行内代码连续性门禁
|
||||
|
||||
段落前\`mdtp_ic_p\`段落后
|
||||
|
||||
- 列表前\`mdtp_ic_l\`列表后
|
||||
|
||||
| 门禁 | 内容 |
|
||||
| --- | --- |
|
||||
| 行内代码 | 单元格前\`mdtp_ic_c\`单元格后 |
|
||||
`;
|
||||
|
||||
const contentTypes = new Map([
|
||||
[".css", "text/css; charset=utf-8"],
|
||||
[".html", "text/html; charset=utf-8"],
|
||||
@@ -226,6 +260,35 @@ function inspectDocx(content) {
|
||||
count + (paragraph.match(/<w:tab\s*\/>/gu)?.length ?? 0),
|
||||
0
|
||||
);
|
||||
const inlineCodeContinuity = inlineCodeContinuityProbes.map((probe) => {
|
||||
const matchingParagraphs = paragraphs.filter((paragraph) =>
|
||||
paragraph.includes(probe.code)
|
||||
);
|
||||
const paragraph = matchingParagraphs[0] ?? "";
|
||||
const text = [...paragraph.matchAll(/<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>/gu)]
|
||||
.map((match) => match[1])
|
||||
.join("");
|
||||
const codeRunPattern = new RegExp(
|
||||
`<w:r(?:\\s|>)[\\s\\S]*?<w:rStyle\\s+w:val="VerbatimChar"\\s*\\/>[\\s\\S]*?<w:t(?:\\s[^>]*)?>${probe.code}<\\/w:t>[\\s\\S]*?<\\/w:r>`,
|
||||
"u"
|
||||
);
|
||||
const hardBreakCount =
|
||||
paragraph.match(/<w:br(?:\s[^>]*)?\s*\/>/gu)?.length ?? 0;
|
||||
return {
|
||||
id: probe.id,
|
||||
code: probe.code,
|
||||
expectedText: probe.text,
|
||||
actualText: text,
|
||||
paragraphCount: matchingParagraphs.length,
|
||||
hardBreakCount,
|
||||
characterStylePresent: codeRunPattern.test(paragraph),
|
||||
passed:
|
||||
matchingParagraphs.length === 1 &&
|
||||
text === probe.text &&
|
||||
hardBreakCount === 0 &&
|
||||
codeRunPattern.test(paragraph)
|
||||
};
|
||||
});
|
||||
return {
|
||||
bytes: content.byteLength,
|
||||
sha256: sha256(content),
|
||||
@@ -247,6 +310,10 @@ function inspectDocx(content) {
|
||||
officialIssueRowRightTabCount:
|
||||
officialIssueRowsWithRightTab.length,
|
||||
officialIssueRowContentTabCount,
|
||||
inlineCodeContinuity,
|
||||
inlineCodeContinuityPassed: inlineCodeContinuity.every(
|
||||
(probe) => probe.passed
|
||||
),
|
||||
requiredStylesPresent: [
|
||||
"Normal",
|
||||
"Heading1",
|
||||
@@ -366,13 +433,22 @@ const port = await reservePort();
|
||||
const origin = `http://127.0.0.1:${port}`;
|
||||
const pdfGenerator = createPdfGenerator({ renderOrigin: origin });
|
||||
const docxMediaAdapter = createDocxMediaEngine({ renderOrigin: origin });
|
||||
const captureDocxMedia = docxMediaAdapter.capture.bind(docxMediaAdapter);
|
||||
let capturedDocxDocumentLayout;
|
||||
docxMediaAdapter.capture = async (...args) => {
|
||||
const output = await captureDocxMedia(...args);
|
||||
capturedDocxDocumentLayout = structuredClone(
|
||||
output.plan.documentLayout ?? { tables: [] }
|
||||
);
|
||||
return output;
|
||||
};
|
||||
const app = buildApp({
|
||||
logger: { level: "error" },
|
||||
pdfGenerator,
|
||||
docxMediaAdapter,
|
||||
fontPacks: {
|
||||
roots: [fontPackRoot],
|
||||
appVersion: "0.6.0"
|
||||
appVersion
|
||||
}
|
||||
});
|
||||
registerWebRuntime(app);
|
||||
@@ -390,10 +466,11 @@ try {
|
||||
);
|
||||
|
||||
for (const theme of themes) {
|
||||
capturedDocxDocumentLayout = undefined;
|
||||
process.stderr.write(`[DOCX R4 matrix] generating ${theme.id}\n`);
|
||||
const samplePath = path.join(samplesDirectory, `${theme.id}.md`);
|
||||
assert(fs.existsSync(samplePath), `主题缺少验收示例:${theme.id}`);
|
||||
const markdown = fs.readFileSync(samplePath, "utf8");
|
||||
const markdown = `${fs.readFileSync(samplePath, "utf8").trimEnd()}${inlineCodeContinuityMarkdown}`;
|
||||
const exportConfig = mergeExportConfig({
|
||||
...defaultExportConfig,
|
||||
name: `${theme.name} R4 生产链验收`,
|
||||
@@ -449,6 +526,10 @@ try {
|
||||
inspection.requiredStylesPresent,
|
||||
`主题 ${theme.id} 缺少标准 Word 样式`
|
||||
);
|
||||
assert(
|
||||
inspection.inlineCodeContinuityPassed,
|
||||
`主题 ${theme.id} 的行内代码连续性门禁失败:${JSON.stringify(inspection.inlineCodeContinuity)}`
|
||||
);
|
||||
const standard = standardByTheme.get(theme.id);
|
||||
assert(standard, `标准矩阵缺少主题 ${theme.id}`);
|
||||
assert(
|
||||
@@ -504,6 +585,7 @@ try {
|
||||
docx: {
|
||||
outputFile: path.relative(repositoryDirectory, docxPath),
|
||||
inspection,
|
||||
documentLayout: capturedDocxDocumentLayout ?? { tables: [] },
|
||||
diagnostics: responseDiagnostics(docx.response, "docx")
|
||||
},
|
||||
standardInspection: standard.inspection,
|
||||
|
||||
@@ -10,7 +10,7 @@ const app = buildApp({
|
||||
? {
|
||||
fontPacks: {
|
||||
roots: [fontPackRoot],
|
||||
appVersion: process.env.APP_VERSION ?? "0.6.0"
|
||||
appVersion: process.env.APP_VERSION ?? "0.6.1"
|
||||
}
|
||||
}
|
||||
: {})
|
||||
|
||||
@@ -38,6 +38,7 @@ export async function captureDocxMediaWithPlaywrightPage(
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
signal?.throwIfAborted();
|
||||
await page.emulateMedia({ media: "print" });
|
||||
const plan = await renderCapturePlan(page, request);
|
||||
signal?.throwIfAborted();
|
||||
const session = await page.context().newCDPSession(page);
|
||||
|
||||
@@ -14,7 +14,7 @@ function readProjectFile(relativePath: string) {
|
||||
return readFileSync(`${projectRoot}/${relativePath}`, "utf8");
|
||||
}
|
||||
|
||||
describe("Docker v0.6.0 部署契约", () => {
|
||||
describe("Docker v0.6.1 部署契约", () => {
|
||||
it("完整构建并复制 DOCX 运行时工作区和 Lua Filter", () => {
|
||||
const dockerfile = readProjectFile("deploy/Dockerfile");
|
||||
const workspaces = [
|
||||
@@ -57,6 +57,9 @@ describe("Docker v0.6.0 部署契约", () => {
|
||||
expect(dockerfile).toContain(
|
||||
"COPY --from=pandoc-runtime /opt/pandoc/3.9.0.2 /opt/pandoc/3.9.0.2"
|
||||
);
|
||||
expect(dockerfile).toContain(
|
||||
"ln -sfn /opt/pandoc/3.9.0.2/bin/pandoc /usr/local/bin/pandoc"
|
||||
);
|
||||
expect(dockerfile).toContain(
|
||||
'test "$(pandoc --version | head -n 1)" = "pandoc 3.9.0.2"'
|
||||
);
|
||||
@@ -65,7 +68,8 @@ describe("Docker v0.6.0 部署契约", () => {
|
||||
it("将受校验字体包内置到镜像并取消运行时字体挂载", () => {
|
||||
const dockerfile = readProjectFile("deploy/Dockerfile");
|
||||
const compose = readProjectFile("deploy/compose.yaml");
|
||||
const dockerignore = readProjectFile(
|
||||
const dockerignore = readProjectFile(".dockerignore");
|
||||
const dockerfileIgnore = readProjectFile(
|
||||
"deploy/Dockerfile.dockerignore"
|
||||
);
|
||||
|
||||
@@ -78,10 +82,11 @@ describe("Docker v0.6.0 部署契约", () => {
|
||||
);
|
||||
expect(dockerfile).toContain("FONT_PACK_ROOT=/app/.local/font-packs");
|
||||
expect(compose).toContain(
|
||||
'APP_VERSION: "${MD_TO_PDF_APP_VERSION:-0.6.0}"'
|
||||
'APP_VERSION: "${MD_TO_PDF_APP_VERSION:-0.6.1}"'
|
||||
);
|
||||
expect(dockerignore.split(/\r?\n/u)).toContain(".local");
|
||||
expect(dockerignore).toContain("!output/font-packs/root/**");
|
||||
expect(dockerfileIgnore).toBe(dockerignore);
|
||||
});
|
||||
|
||||
it("提供显式真实 Docker 字体包验收命令", () => {
|
||||
|
||||
@@ -46,6 +46,7 @@ describe("Server DOCX 媒体浏览器", () => {
|
||||
const detach = vi.fn(async () => undefined);
|
||||
const page = {
|
||||
goto: vi.fn(async () => undefined),
|
||||
emulateMedia: vi.fn(async () => undefined),
|
||||
waitForFunction: vi.fn(async () => undefined),
|
||||
evaluate: vi.fn(async () => emptyPlan),
|
||||
context: () => context
|
||||
@@ -104,6 +105,7 @@ describe("Server DOCX 媒体浏览器", () => {
|
||||
"http://renderer.test/preview-frame.html?target=continuous",
|
||||
{ waitUntil: "domcontentloaded" }
|
||||
);
|
||||
expect(page.emulateMedia).toHaveBeenCalledWith({ media: "print" });
|
||||
expect(continueRoute).toHaveBeenCalledTimes(1);
|
||||
expect(abortRoute).toHaveBeenCalledWith("blockedbyclient");
|
||||
expect(detach).toHaveBeenCalledOnce();
|
||||
|
||||
@@ -62,6 +62,7 @@ describe("Playwright DOCX 媒体捕获", () => {
|
||||
data: Buffer.from("png").toString("base64")
|
||||
}));
|
||||
const page = {
|
||||
emulateMedia: vi.fn(async () => undefined),
|
||||
evaluate: vi.fn(async () => plan),
|
||||
context: () => ({
|
||||
newCDPSession: vi.fn(async () => ({ send, detach }))
|
||||
@@ -74,6 +75,7 @@ describe("Playwright DOCX 媒体捕获", () => {
|
||||
);
|
||||
|
||||
expect(result.plan).toBe(plan);
|
||||
expect(page.emulateMedia).toHaveBeenCalledWith({ media: "print" });
|
||||
expect(result.captures[0]).toEqual({
|
||||
id: "docx-media-1",
|
||||
png: Buffer.from("png")
|
||||
|
||||
@@ -3,13 +3,13 @@ import {
|
||||
isPagedPreviewRenderRequest,
|
||||
PAGED_PREVIEW_MESSAGE_SCOPE,
|
||||
PagedDocumentRuntime,
|
||||
SHARED_HIGHLIGHT_CSS,
|
||||
renderDocxMediaCapturePlan,
|
||||
resolveMermaidOutputMode,
|
||||
resolvePagedRenderTarget,
|
||||
type PagedPreviewFrameMessage,
|
||||
type PagedPreviewPayload
|
||||
} from "@md-to-pdf/preview-engine";
|
||||
import highlightCss from "highlight.js/styles/github.css?inline";
|
||||
import katexCss from "katex/dist/katex.min.css?inline";
|
||||
import {
|
||||
beginPreviewFrameRender,
|
||||
@@ -29,7 +29,7 @@ const previewRoot = getPreviewRoot();
|
||||
const renderTarget = resolvePagedRenderTarget(window.location.search);
|
||||
const mermaidOutput = resolveMermaidOutputMode(window.location.search);
|
||||
const runtime = new PagedDocumentRuntime(previewRoot, {
|
||||
highlightCss,
|
||||
highlightCss: SHARED_HIGHLIGHT_CSS,
|
||||
katexCss,
|
||||
echartsCss
|
||||
});
|
||||
|
||||
@@ -10,8 +10,8 @@ const webRoot = fileURLToPath(new URL("../", import.meta.url));
|
||||
|
||||
describe("应用版本", () => {
|
||||
it("从统一构建版本生成标题徽标", () => {
|
||||
expect(APP_VERSION).toBe("0.6.0");
|
||||
expect(APP_VERSION_LABEL).toBe("v0.6.0");
|
||||
expect(APP_VERSION).toBe("0.6.1");
|
||||
expect(APP_VERSION_LABEL).toBe("v0.6.1");
|
||||
});
|
||||
|
||||
it("允许浏览器加载同源 Web Manifest", () => {
|
||||
|
||||
Reference in New Issue
Block a user