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:
@@ -1,7 +1,15 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { collectDocxMediaCaptureTargets } from "../src/index.js";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
collectDocxDocumentLayoutPlan,
|
||||
collectDocxInlineCodeLayouts,
|
||||
collectDocxListItemLayouts,
|
||||
collectDocxTextBlockLayouts,
|
||||
collectDocxMediaCaptureTargets,
|
||||
removeInheritedPagedSplitJustification,
|
||||
renderDocxMediaCapturePlan
|
||||
} from "../src/index.js";
|
||||
|
||||
describe("DOCX 媒体捕获计划", () => {
|
||||
beforeEach(() => {
|
||||
@@ -9,6 +17,74 @@ describe("DOCX 媒体捕获计划", () => {
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
|
||||
it("连续布局使用原始打印媒体主题 CSS", async () => {
|
||||
const renderContinuous = vi.fn(async () => ({
|
||||
echartsErrors: [],
|
||||
mermaidErrors: []
|
||||
}));
|
||||
const root = document.createElement("main");
|
||||
root.innerHTML = '<article id="write"></article>';
|
||||
|
||||
await renderDocxMediaCapturePlan(
|
||||
{ renderContinuous } as never,
|
||||
root,
|
||||
{
|
||||
articleHtml: '<article id="write"></article>',
|
||||
fileName: "打印媒体.md",
|
||||
metadata: {
|
||||
title: "",
|
||||
author: "",
|
||||
subject: "",
|
||||
keywords: [],
|
||||
language: "zh-CN"
|
||||
},
|
||||
semanticDocument: {
|
||||
schemaVersion: 1,
|
||||
titlePolicy: {
|
||||
metadataTitle: "suppress",
|
||||
firstBodyHeading: "keep"
|
||||
},
|
||||
regions: []
|
||||
},
|
||||
features: [],
|
||||
themeCss: "@media print { html { font-size: 13px; } }",
|
||||
exportConfig: {} as never
|
||||
},
|
||||
{ contentWidthPx: 640, contentHeightPx: 900 }
|
||||
);
|
||||
|
||||
expect(renderContinuous).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({ themeMedia: "print" })
|
||||
);
|
||||
});
|
||||
|
||||
it("只移除会把 Paged.js 末行两端对齐继承给子块的容器标记", () => {
|
||||
document.body.innerHTML = `
|
||||
<main id="pages">
|
||||
<article id="write" data-align-last-split-element="justify">
|
||||
<h1>标题</h1>
|
||||
<p data-align-last-split-element="justify">跨页正文</p>
|
||||
</article>
|
||||
</main>
|
||||
`;
|
||||
expect(
|
||||
removeInheritedPagedSplitJustification(
|
||||
document.querySelector("#pages")!
|
||||
)
|
||||
).toBe(1);
|
||||
expect(
|
||||
document.querySelector("#write")?.hasAttribute(
|
||||
"data-align-last-split-element"
|
||||
)
|
||||
).toBe(false);
|
||||
expect(
|
||||
document.querySelector("p")?.getAttribute(
|
||||
"data-align-last-split-element"
|
||||
)
|
||||
).toBe("justify");
|
||||
});
|
||||
|
||||
it("按文档顺序标记图片、Mermaid 和 ECharts", () => {
|
||||
document.body.innerHTML = `
|
||||
<article id="write">
|
||||
@@ -155,4 +231,236 @@ describe("DOCX 媒体捕获计划", () => {
|
||||
).toBeLessThanOrEqual(4096);
|
||||
expect(target?.altText).toBe("图片 1");
|
||||
});
|
||||
|
||||
it("从真实 DOM 几何采集主题无关的表格列宽比例", () => {
|
||||
document.body.innerHTML = `
|
||||
<article id="write">
|
||||
<table><tbody>
|
||||
<tr><td>短列</td><td>较长内容列</td></tr>
|
||||
<tr><td>1</td><td>2</td></tr>
|
||||
</tbody></table>
|
||||
</article>
|
||||
`;
|
||||
const article = document.querySelector<HTMLElement>("#write")!;
|
||||
const table = document.querySelector<HTMLTableElement>("table")!;
|
||||
const cells = Array.from(table.querySelectorAll<HTMLTableCellElement>("td"));
|
||||
article.getBoundingClientRect = () => ({
|
||||
left: 100,
|
||||
right: 900,
|
||||
top: 0,
|
||||
bottom: 900,
|
||||
width: 800,
|
||||
height: 900
|
||||
}) as DOMRect;
|
||||
table.getBoundingClientRect = () => ({
|
||||
left: 140,
|
||||
right: 860,
|
||||
top: 20,
|
||||
bottom: 220,
|
||||
width: 720,
|
||||
height: 200
|
||||
}) as DOMRect;
|
||||
cells.forEach((cell, index) => {
|
||||
const firstColumn = index % 2 === 0;
|
||||
cell.getBoundingClientRect = () => ({
|
||||
left: firstColumn ? 140 : 356,
|
||||
right: firstColumn ? 356 : 860,
|
||||
top: index < 2 ? 20 : 120,
|
||||
bottom: index < 2 ? 120 : 220,
|
||||
width: firstColumn ? 216 : 504,
|
||||
height: 100
|
||||
}) as DOMRect;
|
||||
});
|
||||
|
||||
expect(
|
||||
collectDocxDocumentLayoutPlan(document, {
|
||||
contentWidthPx: 800,
|
||||
contentHeightPx: 900
|
||||
})
|
||||
).toEqual({
|
||||
inlineCodes: [],
|
||||
listItems: [],
|
||||
textBlocks: [],
|
||||
tables: [
|
||||
{
|
||||
ordinal: 1,
|
||||
widthPercent: 90,
|
||||
leftOffsetPercent: 5,
|
||||
columnWidthPercents: [30, 70],
|
||||
rows: [
|
||||
{
|
||||
cells: [
|
||||
{
|
||||
columnSpan: 1,
|
||||
backgroundColor: "#ffffff",
|
||||
color: "#000000",
|
||||
bold: false,
|
||||
italic: false,
|
||||
alignment: "left"
|
||||
},
|
||||
{
|
||||
columnSpan: 1,
|
||||
backgroundColor: "#ffffff",
|
||||
color: "#000000",
|
||||
bold: false,
|
||||
italic: false,
|
||||
alignment: "left"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
cells: [
|
||||
{
|
||||
columnSpan: 1,
|
||||
backgroundColor: "#ffffff",
|
||||
color: "#000000",
|
||||
bold: false,
|
||||
italic: false,
|
||||
alignment: "left"
|
||||
},
|
||||
{
|
||||
columnSpan: 1,
|
||||
backgroundColor: "#ffffff",
|
||||
color: "#000000",
|
||||
bold: false,
|
||||
italic: false,
|
||||
alignment: "left"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it("按真实上下文采集行内代码字号、颜色和盒模型", () => {
|
||||
document.body.innerHTML = `
|
||||
<article id="write" style="font-size: 20px">
|
||||
<p><code style="font-size: .8em; letter-spacing: 1px; color: rgb(17, 34, 51); background: rgb(245, 245, 245); padding: 2px 4px">inline</code></p>
|
||||
<pre><code>block</code></pre>
|
||||
</article>
|
||||
`;
|
||||
expect(
|
||||
collectDocxInlineCodeLayouts(
|
||||
document.querySelector<HTMLElement>("#write")!
|
||||
)
|
||||
).toEqual([
|
||||
{
|
||||
ordinal: 1,
|
||||
text: "inline",
|
||||
fontSizePt: 12,
|
||||
letterSpacingPt: 0.75,
|
||||
color: "#112233",
|
||||
backgroundColor: "#f5f5f5",
|
||||
paddingPt: {
|
||||
top: 1.5,
|
||||
right: 3,
|
||||
bottom: 1.5,
|
||||
left: 3
|
||||
},
|
||||
borderPt: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0
|
||||
}
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("按列表项实际文字起点采集嵌套缩进且排除子列表文字", () => {
|
||||
document.body.innerHTML = `
|
||||
<article id="write">
|
||||
<ul><li>一级 <code>代码</code><ul><li>二级项目</li></ul></li></ul>
|
||||
</article>
|
||||
`;
|
||||
const article = document.querySelector<HTMLElement>("#write")!;
|
||||
article.getBoundingClientRect = () => ({
|
||||
left: 100,
|
||||
right: 900,
|
||||
top: 0,
|
||||
bottom: 900,
|
||||
width: 800,
|
||||
height: 900
|
||||
}) as DOMRect;
|
||||
const rangePrototype = Object.getPrototypeOf(document.createRange()) as {
|
||||
getBoundingClientRect?: () => DOMRect;
|
||||
};
|
||||
const original = rangePrototype.getBoundingClientRect;
|
||||
rangePrototype.getBoundingClientRect = function (this: Range) {
|
||||
const left = this.startContainer.textContent === "二级项目" ? 340 : 220;
|
||||
return {
|
||||
left,
|
||||
right: left + 10,
|
||||
top: 20,
|
||||
bottom: 32,
|
||||
width: 10,
|
||||
height: 12
|
||||
} as DOMRect;
|
||||
};
|
||||
try {
|
||||
expect(collectDocxListItemLayouts(article)).toEqual([
|
||||
{
|
||||
ordinal: 1,
|
||||
text: "一级 代码",
|
||||
depth: 0,
|
||||
textStartPt: 90
|
||||
},
|
||||
{
|
||||
ordinal: 2,
|
||||
text: "二级项目",
|
||||
depth: 1,
|
||||
textStartPt: 180
|
||||
}
|
||||
]);
|
||||
} finally {
|
||||
rangePrototype.getBoundingClientRect = original;
|
||||
}
|
||||
});
|
||||
|
||||
it("采集正文文本块在 Chromium 中的实际行断点", () => {
|
||||
document.body.innerHTML = `
|
||||
<article id="write">
|
||||
<section data-semantic-region="cover"><p>独立封面</p></section>
|
||||
<p>甲乙丙丁</p>
|
||||
</article>
|
||||
`;
|
||||
const rangePrototype = Object.getPrototypeOf(document.createRange()) as {
|
||||
getBoundingClientRect?: () => DOMRect;
|
||||
};
|
||||
const original = rangePrototype.getBoundingClientRect;
|
||||
rangePrototype.getBoundingClientRect = function (this: Range) {
|
||||
const top = this.startOffset < 2 ? 100 : 120;
|
||||
return {
|
||||
x: this.startOffset * 10,
|
||||
y: top,
|
||||
left: this.startOffset * 10,
|
||||
right: this.startOffset * 10 + 10,
|
||||
top,
|
||||
bottom: top + 12,
|
||||
width: 10,
|
||||
height: 12,
|
||||
toJSON: () => ({})
|
||||
} as DOMRect;
|
||||
};
|
||||
try {
|
||||
expect(
|
||||
collectDocxTextBlockLayouts(
|
||||
document.querySelector<HTMLElement>("#write")!
|
||||
)
|
||||
).toEqual([
|
||||
{
|
||||
ordinal: 1,
|
||||
text: "甲乙丙丁",
|
||||
letterSpacingPt: 0,
|
||||
alignment: "left",
|
||||
linePitchPt: 15,
|
||||
lineBreakOffsets: [2]
|
||||
}
|
||||
]);
|
||||
} finally {
|
||||
rangePrototype.getBoundingClientRect = original;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user