release: 发布 v0.6.2 DOCX 真实文档修复
新增能力:将 DOCX 发布验收拆分为四套独立 140,支持真实语料冻结、指纹复用、失败与基础设施错误独立统计,并为表格换行、全 JSON 围栏、代码连续性和长文档分页建立通用门禁。 问题修复:冻结 Paged.js 分片前的逻辑表格列轨并传递打印几何,统一 Markdown 表格换行、代码、段落与 OOXML 翻译;改进 PDF 文本流排序、语义块映射、颜色与栅格比较,消除窄字符重叠和跨行范围符号误报。 兼容与部署:版本统一为 0.6.2;正式 Docker 镜像内置固定 Chromium、Pandoc 3.9.0.2 和 Serif/Sans/Mono 字体;Desktop NSIS 与 ZIP 继续直接内置字体,无需系统字体安装。 验证结果:合成基线与长庆严格 280/280,M4N 140/140;健康数据残余误报 6/115(5.22%),均核查为重复表头自动对齐/取样误报且基础设施错误为 0。全项目测试、类型检查、生产构建和 git diff --check 通过;正式 Docker、NSIS、ZIP、离线镜像、部署包、清单及 SHA-256 均已生成并校验。
This commit is contained in:
@@ -98,6 +98,32 @@ describe("代码块分页预处理", () => {
|
||||
).toBe("return");
|
||||
});
|
||||
|
||||
it("将高亮代码行首缩进封装为分页稳定节点", () => {
|
||||
const root = createRoot(
|
||||
'<span class="hljs-punctuation">{</span>\n ' +
|
||||
'<span class="hljs-attr">"nested"</span>: true\n' +
|
||||
'<span class="hljs-punctuation">}</span>\n'
|
||||
);
|
||||
|
||||
prepareCodeBlockPagination(root);
|
||||
|
||||
const lines = getLines(root);
|
||||
const indent = lines[1]?.querySelector(
|
||||
":scope > .md-code-line-indent"
|
||||
);
|
||||
expect(lines.map((line) => line.textContent)).toEqual([
|
||||
"{",
|
||||
' "nested": true',
|
||||
"}"
|
||||
]);
|
||||
expect(indent?.textContent).toBe(" ");
|
||||
expect((indent as HTMLElement | null)?.style.width).toBe("2ch");
|
||||
expect((indent as HTMLElement | null)?.dataset.codeIndentColumns)
|
||||
.toBe("2");
|
||||
expect(indent?.nextElementSibling?.classList.contains("hljs-attr"))
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
it("重复调用不会再次包装已经准备的代码块", () => {
|
||||
const root = createRoot("line-1\nline-2\n");
|
||||
|
||||
|
||||
@@ -2,13 +2,21 @@
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
collectPagedDocxTableGeometries,
|
||||
collectDocxDocumentLayoutPlan,
|
||||
collectDocxInlineCodeLayouts,
|
||||
collectDocxListItemLayouts,
|
||||
collectDocxTextBlockLayouts,
|
||||
collectDocxMediaCaptureTargets,
|
||||
forcePagedTableRowsToNextPage,
|
||||
mergePagedTableGeometries,
|
||||
missingPagedTableRowIds,
|
||||
protectFittableInlineCodeParagraphs,
|
||||
removeInheritedPagedSplitJustification,
|
||||
renderDocxMediaCapturePlan
|
||||
renderDocxMediaCapturePlan,
|
||||
restorePagedTableColumns,
|
||||
restorePagedTableStructure,
|
||||
stabilizePagedTableColumns
|
||||
} from "../src/index.js";
|
||||
|
||||
describe("DOCX 媒体捕获计划", () => {
|
||||
@@ -18,6 +26,10 @@ describe("DOCX 媒体捕获计划", () => {
|
||||
});
|
||||
|
||||
it("连续布局使用原始打印媒体主题 CSS", async () => {
|
||||
const render = vi.fn(async () => ({
|
||||
echartsErrors: [],
|
||||
mermaidErrors: []
|
||||
}));
|
||||
const renderContinuous = vi.fn(async () => ({
|
||||
echartsErrors: [],
|
||||
mermaidErrors: []
|
||||
@@ -26,7 +38,7 @@ describe("DOCX 媒体捕获计划", () => {
|
||||
root.innerHTML = '<article id="write"></article>';
|
||||
|
||||
await renderDocxMediaCapturePlan(
|
||||
{ renderContinuous } as never,
|
||||
{ render, renderContinuous } as never,
|
||||
root,
|
||||
{
|
||||
articleHtml: '<article id="write"></article>',
|
||||
@@ -57,6 +69,155 @@ describe("DOCX 媒体捕获计划", () => {
|
||||
expect.anything(),
|
||||
expect.objectContaining({ themeMedia: "print" })
|
||||
);
|
||||
expect(render).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
{
|
||||
target: "pdf",
|
||||
allowIncompleteTableGeometry: true
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("仅将能完整放入一页的顶层行内代码段落保持为整体", () => {
|
||||
document.body.innerHTML = `
|
||||
<article id="write">
|
||||
<p id="short" class="md-inline-code-paragraph">短段落</p>
|
||||
<p id="long" class="md-inline-code-paragraph">超长段落</p>
|
||||
<table><tbody><tr><td>
|
||||
<p id="cell" class="md-inline-code-paragraph">单元格段落</p>
|
||||
</td></tr></tbody></table>
|
||||
</article>
|
||||
`;
|
||||
const short = document.querySelector<HTMLElement>("#short")!;
|
||||
const long = document.querySelector<HTMLElement>("#long")!;
|
||||
const cell = document.querySelector<HTMLElement>("#cell")!;
|
||||
short.getBoundingClientRect = () => ({ height: 240 }) as DOMRect;
|
||||
long.getBoundingClientRect = () => ({ height: 1200 }) as DOMRect;
|
||||
cell.getBoundingClientRect = () => ({ height: 120 }) as DOMRect;
|
||||
|
||||
expect(protectFittableInlineCodeParagraphs(document, 900)).toBe(1);
|
||||
expect(short.style.getPropertyValue("break-inside")).toBe("avoid");
|
||||
expect(short.style.getPropertyPriority("break-inside")).toBe("important");
|
||||
expect(short.dataset.mdtpInlineCodeKeepWhole).toBe("true");
|
||||
expect(long.style.getPropertyValue("break-inside")).toBe("");
|
||||
expect(cell.style.getPropertyValue("break-inside")).toBe("");
|
||||
});
|
||||
|
||||
it("按逻辑表格去重采集分页后的真实列轨", () => {
|
||||
document.body.innerHTML = `
|
||||
<main class="pagedjs_pages">
|
||||
<section class="pagedjs_page">
|
||||
<div class="pagedjs_area">
|
||||
<table data-ref="table-a">
|
||||
<colgroup data-stabilized-columns="true">
|
||||
<col data-stabilized-width-px="120">
|
||||
<col data-stabilized-width-px="280">
|
||||
</colgroup>
|
||||
<tbody><tr><td>A</td><td>B</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pagedjs_page">
|
||||
<div class="pagedjs_area">
|
||||
<table data-ref="table-a">
|
||||
<colgroup data-stabilized-columns="true">
|
||||
<col data-stabilized-width-px="120">
|
||||
<col data-stabilized-width-px="280">
|
||||
</colgroup>
|
||||
<tbody><tr><td>C</td><td>D</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
`;
|
||||
const areas = Array.from(
|
||||
document.querySelectorAll<HTMLElement>(".pagedjs_area")
|
||||
);
|
||||
const tables = Array.from(
|
||||
document.querySelectorAll<HTMLTableElement>("table")
|
||||
);
|
||||
areas.forEach((area) => {
|
||||
area.getBoundingClientRect = () => ({
|
||||
left: 100, right: 900, top: 0, bottom: 900,
|
||||
width: 800, height: 900
|
||||
}) as DOMRect;
|
||||
});
|
||||
tables.forEach((table, index) => {
|
||||
table.getBoundingClientRect = () => ({
|
||||
left: index === 0 ? 900 : 140,
|
||||
right: index === 0 ? 1300 : 540,
|
||||
top: 20, bottom: 220,
|
||||
width: 400, height: 200
|
||||
}) as DOMRect;
|
||||
});
|
||||
|
||||
expect(collectPagedDocxTableGeometries(document, {
|
||||
contentWidthPx: 800,
|
||||
contentHeightPx: 900
|
||||
})).toEqual([{
|
||||
ordinal: 1,
|
||||
widthPercent: 50,
|
||||
leftOffsetPercent: 5,
|
||||
columnWidthPercents: [30, 70]
|
||||
}]);
|
||||
});
|
||||
|
||||
it("只覆盖列数一致的分页表格几何并保留连续布局行样式", () => {
|
||||
const row = {
|
||||
cells: [{
|
||||
columnSpan: 1,
|
||||
backgroundColor: "#ffffff",
|
||||
color: "#000000",
|
||||
bold: true,
|
||||
italic: false,
|
||||
alignment: "left" as const
|
||||
}]
|
||||
};
|
||||
const layout = {
|
||||
tables: [
|
||||
{
|
||||
ordinal: 1,
|
||||
widthPercent: 100,
|
||||
leftOffsetPercent: 0,
|
||||
columnWidthPercents: [40, 60],
|
||||
rows: [row]
|
||||
},
|
||||
{
|
||||
ordinal: 2,
|
||||
widthPercent: 100,
|
||||
leftOffsetPercent: 0,
|
||||
columnWidthPercents: [100],
|
||||
rows: [row]
|
||||
}
|
||||
],
|
||||
textBlocks: [],
|
||||
listItems: [],
|
||||
inlineCodes: []
|
||||
};
|
||||
|
||||
const merged = mergePagedTableGeometries(layout, [
|
||||
{
|
||||
ordinal: 1,
|
||||
widthPercent: 90,
|
||||
leftOffsetPercent: 5,
|
||||
columnWidthPercents: [30, 70]
|
||||
},
|
||||
{
|
||||
ordinal: 2,
|
||||
widthPercent: 80,
|
||||
leftOffsetPercent: 10,
|
||||
columnWidthPercents: [20, 80]
|
||||
}
|
||||
]);
|
||||
|
||||
expect(merged.tables[0]).toEqual({
|
||||
...layout.tables[0],
|
||||
widthPercent: 90,
|
||||
leftOffsetPercent: 5,
|
||||
columnWidthPercents: [30, 70]
|
||||
});
|
||||
expect(merged.tables[0]?.rows).toBe(layout.tables[0]?.rows);
|
||||
expect(merged.tables[1]).toBe(layout.tables[1]);
|
||||
});
|
||||
|
||||
it("只移除会把 Paged.js 末行两端对齐继承给子块的容器标记", () => {
|
||||
@@ -334,6 +495,382 @@ describe("DOCX 媒体捕获计划", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("保留主题表格超出文档内容盒的实测宽度", () => {
|
||||
document.body.innerHTML = `
|
||||
<article id="write"><table><tbody><tr><td>内容</td></tr></tbody></table></article>
|
||||
`;
|
||||
const article = document.querySelector<HTMLElement>("#write")!;
|
||||
const table = document.querySelector<HTMLTableElement>("table")!;
|
||||
const cell = document.querySelector<HTMLTableCellElement>("td")!;
|
||||
article.getBoundingClientRect = () => ({
|
||||
left: 100,
|
||||
right: 900,
|
||||
top: 0,
|
||||
bottom: 900,
|
||||
width: 800,
|
||||
height: 900,
|
||||
}) as DOMRect;
|
||||
table.getBoundingClientRect = () => ({
|
||||
left: 100,
|
||||
right: 980,
|
||||
top: 20,
|
||||
bottom: 120,
|
||||
width: 880,
|
||||
height: 100,
|
||||
}) as DOMRect;
|
||||
cell.getBoundingClientRect = table.getBoundingClientRect;
|
||||
|
||||
const layout = collectDocxDocumentLayoutPlan(document, {
|
||||
contentWidthPx: 800,
|
||||
contentHeightPx: 900,
|
||||
});
|
||||
|
||||
expect(layout.tables[0]?.widthPercent).toBeCloseTo(110, 8);
|
||||
expect(layout.tables[0]?.columnWidthPercents).toEqual([100]);
|
||||
});
|
||||
|
||||
it("分页前冻结整表实测列宽供拆分页片段复用", () => {
|
||||
document.body.innerHTML = `
|
||||
<article id="write"><table><tbody><tr><td>A</td><td>B</td></tr></tbody></table></article>
|
||||
`;
|
||||
const table = document.querySelector<HTMLTableElement>("table")!;
|
||||
const cells = Array.from(table.rows[0]!.cells);
|
||||
table.getBoundingClientRect = () => ({
|
||||
left: 100,
|
||||
right: 500,
|
||||
top: 0,
|
||||
bottom: 100,
|
||||
width: 400,
|
||||
height: 100
|
||||
}) as DOMRect;
|
||||
cells[0]!.getBoundingClientRect = () => ({
|
||||
left: 100,
|
||||
right: 220,
|
||||
top: 0,
|
||||
bottom: 100,
|
||||
width: 120,
|
||||
height: 100
|
||||
}) as DOMRect;
|
||||
cells[1]!.getBoundingClientRect = () => ({
|
||||
left: 220,
|
||||
right: 500,
|
||||
top: 0,
|
||||
bottom: 100,
|
||||
width: 280,
|
||||
height: 100
|
||||
}) as DOMRect;
|
||||
|
||||
expect(stabilizePagedTableColumns(document)).toBe(1);
|
||||
expect(table.dataset.stabilizedColumns).toBe("true");
|
||||
expect(table.style.tableLayout).toBe("fixed");
|
||||
expect(Array.from(table.querySelectorAll("col")).map(
|
||||
(column) => column.style.width
|
||||
)).toEqual(["120px", "280px"]);
|
||||
expect(Array.from(table.querySelectorAll("col")).map(
|
||||
(column) => (column as HTMLTableColElement).dataset.stabilizedWidthPx
|
||||
)).toEqual(["120", "280"]);
|
||||
expect(Array.from(table.querySelectorAll("th, td")).map(
|
||||
(cell) => (cell as HTMLElement).style.width
|
||||
)).toEqual(["120px", "280px"]);
|
||||
expect(stabilizePagedTableColumns(document)).toBe(0);
|
||||
});
|
||||
|
||||
it("DOCX 布局采集保持分页前原始列轨而不受辅助样式二次测量影响", () => {
|
||||
document.body.innerHTML = `
|
||||
<article id="write">
|
||||
<table data-stabilized-columns="true">
|
||||
<colgroup data-stabilized-columns="true">
|
||||
<col data-stabilized-width-px="120" style="width: 140px">
|
||||
<col data-stabilized-width-px="280" style="width: 260px">
|
||||
</colgroup>
|
||||
<tbody><tr><td>A</td><td>B</td></tr></tbody>
|
||||
</table>
|
||||
</article>
|
||||
`;
|
||||
const article = document.querySelector<HTMLElement>("#write")!;
|
||||
const table = document.querySelector<HTMLTableElement>("table")!;
|
||||
const cells = Array.from(table.rows[0]!.cells);
|
||||
article.getBoundingClientRect = () => ({
|
||||
left: 100, right: 500, top: 0, bottom: 100,
|
||||
width: 400, height: 100
|
||||
}) as DOMRect;
|
||||
table.getBoundingClientRect = article.getBoundingClientRect;
|
||||
cells[0]!.getBoundingClientRect = () => ({
|
||||
left: 100, right: 240, top: 0, bottom: 100,
|
||||
width: 140, height: 100
|
||||
}) as DOMRect;
|
||||
cells[1]!.getBoundingClientRect = () => ({
|
||||
left: 240, right: 500, top: 0, bottom: 100,
|
||||
width: 260, height: 100
|
||||
}) as DOMRect;
|
||||
|
||||
const layout = collectDocxDocumentLayoutPlan(document, {
|
||||
contentWidthPx: 400,
|
||||
contentHeightPx: 900
|
||||
});
|
||||
|
||||
expect(layout.tables[0]?.columnWidthPercents).toEqual([30, 70]);
|
||||
});
|
||||
|
||||
it("冻结跨列单元格为对应列轨宽度之和", () => {
|
||||
document.body.innerHTML = `
|
||||
<table><tbody>
|
||||
<tr><td colspan="2">跨列</td></tr>
|
||||
<tr><td>甲</td><td>乙</td></tr>
|
||||
</tbody></table>
|
||||
`;
|
||||
const table = document.querySelector("table")!;
|
||||
const cells = Array.from(table.querySelectorAll("td"));
|
||||
table.getBoundingClientRect = () => ({
|
||||
left: 100, right: 500, top: 0, bottom: 100,
|
||||
width: 400, height: 100
|
||||
}) as DOMRect;
|
||||
cells[0]!.getBoundingClientRect = () => ({
|
||||
left: 100, right: 500, top: 0, bottom: 50,
|
||||
width: 400, height: 50
|
||||
}) as DOMRect;
|
||||
cells[1]!.getBoundingClientRect = () => ({
|
||||
left: 100, right: 220, top: 50, bottom: 100,
|
||||
width: 120, height: 50
|
||||
}) as DOMRect;
|
||||
cells[2]!.getBoundingClientRect = () => ({
|
||||
left: 220, right: 500, top: 50, bottom: 100,
|
||||
width: 280, height: 50
|
||||
}) as DOMRect;
|
||||
|
||||
expect(stabilizePagedTableColumns(document)).toBe(1);
|
||||
expect(cells.map((cell) => cell.style.width)).toEqual([
|
||||
"400px", "120px", "280px"
|
||||
]);
|
||||
});
|
||||
|
||||
it("分页片段已有冻结列定义时不重复插入 colgroup", () => {
|
||||
document.body.innerHTML = `
|
||||
<table id="source">
|
||||
<colgroup data-stabilized-columns="true"><col style="width: 30%"><col style="width: 70%"></colgroup>
|
||||
<thead><tr><th>A</th><th>B</th></tr></thead>
|
||||
<tbody><tr><td>甲</td><td>乙</td></tr></tbody>
|
||||
</table>
|
||||
<table id="rendered">
|
||||
<colgroup data-stabilized-columns="true"><col style="width: 30%"><col style="width: 70%"></colgroup>
|
||||
<tbody><tr><td>丙</td><td>丁</td></tr></tbody>
|
||||
</table>
|
||||
`;
|
||||
const source = document.querySelector<HTMLTableElement>("#source")!;
|
||||
const rendered = document.querySelector<HTMLTableElement>("#rendered")!;
|
||||
|
||||
restorePagedTableStructure(source, rendered);
|
||||
|
||||
expect(rendered.querySelectorAll(":scope > colgroup")).toHaveLength(1);
|
||||
expect(rendered.querySelectorAll(":scope > colgroup > col")).toHaveLength(2);
|
||||
expect(rendered.querySelectorAll(":scope > thead")).toHaveLength(1);
|
||||
expect(Array.from(rendered.children).map((child) => child.tagName)).toEqual([
|
||||
"COLGROUP",
|
||||
"THEAD",
|
||||
"TBODY"
|
||||
]);
|
||||
});
|
||||
|
||||
it("分页前仅对可容纳于单页的表格行单元格启用整行保护", () => {
|
||||
document.body.innerHTML = `
|
||||
<table><tbody>
|
||||
<tr id="normal"><td>普通行</td><td>内容</td></tr>
|
||||
<tr id="tall"><td>超高行</td><td>内容</td></tr>
|
||||
</tbody></table>
|
||||
`;
|
||||
const table = document.querySelector<HTMLTableElement>("table")!;
|
||||
const normal = document.querySelector<HTMLTableRowElement>("#normal")!;
|
||||
const tall = document.querySelector<HTMLTableRowElement>("#tall")!;
|
||||
table.getBoundingClientRect = () => ({
|
||||
width: 400, height: 1020, top: 0, right: 400,
|
||||
bottom: 1020, left: 0
|
||||
}) as DOMRect;
|
||||
normal.getBoundingClientRect = () => ({
|
||||
width: 400, height: 120, top: 0, right: 400,
|
||||
bottom: 120, left: 0
|
||||
}) as DOMRect;
|
||||
tall.getBoundingClientRect = () => ({
|
||||
width: 400, height: 900, top: 120, right: 400,
|
||||
bottom: 1020, left: 0
|
||||
}) as DOMRect;
|
||||
Array.from(table.rows).flatMap((row) => Array.from(row.cells))
|
||||
.forEach((cell, index) => {
|
||||
cell.getBoundingClientRect = () => ({
|
||||
width: 200,
|
||||
height: index < 2 ? 120 : 900,
|
||||
top: index < 2 ? 0 : 120,
|
||||
right: index % 2 === 0 ? 200 : 400,
|
||||
bottom: index < 2 ? 120 : 1020,
|
||||
left: index % 2 === 0 ? 0 : 200
|
||||
}) as DOMRect;
|
||||
});
|
||||
|
||||
stabilizePagedTableColumns(document, 800);
|
||||
|
||||
expect(Array.from(normal.cells).map(
|
||||
(cell) => cell.style.getPropertyValue("break-inside")
|
||||
)).toEqual(["avoid", "avoid"]);
|
||||
expect(Array.from(tall.cells).map(
|
||||
(cell) => cell.style.getPropertyValue("break-inside")
|
||||
)).toEqual(["auto", "auto"]);
|
||||
expect(Array.from(normal.cells).every(
|
||||
(cell) => cell.style.getPropertyPriority("break-inside") === "important"
|
||||
)).toBe(true);
|
||||
expect(normal.dataset.mdtpTableRowId).toBe("table-1-row-1");
|
||||
expect(normal.dataset.mdtpTableRowKeepWhole).toBe("true");
|
||||
expect(tall.dataset.mdtpTableRowId).toBe("table-1-row-2");
|
||||
expect(tall.dataset.mdtpTableRowKeepWhole).toBe("false");
|
||||
});
|
||||
|
||||
it("检测分页后缺失的可容纳正文行并施加恢复断点", () => {
|
||||
document.body.innerHTML = `
|
||||
<main id="source"><table><tbody>
|
||||
<tr data-mdtp-table-row-id="row-1" data-mdtp-table-row-keep-whole="true"><td>甲</td></tr>
|
||||
<tr data-mdtp-table-row-id="row-2" data-mdtp-table-row-keep-whole="true"><td>乙</td></tr>
|
||||
<tr data-mdtp-table-row-id="row-tall" data-mdtp-table-row-keep-whole="false"><td>超高</td></tr>
|
||||
</tbody></table></main>
|
||||
<main id="rendered"><table><tbody>
|
||||
<tr data-mdtp-table-row-id="row-2"><td>乙</td></tr>
|
||||
</tbody></table></main>
|
||||
`;
|
||||
const source = document.querySelector("#source")!;
|
||||
const rendered = document.querySelector("#rendered")!;
|
||||
|
||||
expect(missingPagedTableRowIds(source, rendered)).toEqual(["row-1"]);
|
||||
expect(forcePagedTableRowsToNextPage(source, ["row-1"])).toBe(1);
|
||||
const row = source.querySelector<HTMLTableRowElement>(
|
||||
'[data-mdtp-table-row-id="row-1"]'
|
||||
)!;
|
||||
const table = row.closest("table")!;
|
||||
const marker = table.previousElementSibling as HTMLElement;
|
||||
expect(marker.dataset.mdtpTableRecoveryMarker).toBe("true");
|
||||
expect(marker.textContent).toBe("\u00a0");
|
||||
expect(marker.style.getPropertyValue("height")).toBe("1px");
|
||||
expect(marker.style.getPropertyValue("margin-bottom")).toBe("-1px");
|
||||
expect(marker.style.getPropertyValue("opacity")).toBe("0");
|
||||
expect(marker.style.getPropertyValue("break-before")).toBe("page");
|
||||
expect(marker.style.getPropertyPriority("break-before")).toBe("important");
|
||||
expect(table.dataset.mdtpTableRecovery).toBe("true");
|
||||
expect(row.dataset.mdtpTableRowRecovery).toBe("true");
|
||||
expect(forcePagedTableRowsToNextPage(source, ["row-1"])).toBe(1);
|
||||
expect(source.querySelectorAll("[data-mdtp-table-recovery-marker]"))
|
||||
.toHaveLength(1);
|
||||
|
||||
});
|
||||
|
||||
it("在非首行缺失时拆分续表并将恢复断点放到表格外", () => {
|
||||
document.body.innerHTML = `
|
||||
<main id="source"><table id="source-table">
|
||||
<colgroup><col><col></colgroup>
|
||||
<thead><tr><th>甲</th><th>乙</th></tr></thead>
|
||||
<tbody>
|
||||
<tr data-mdtp-table-row-id="row-1"><td>一</td><td>壹</td></tr>
|
||||
<tr data-mdtp-table-row-id="row-2"><td>二</td><td>贰</td></tr>
|
||||
<tr data-mdtp-table-row-id="row-3"><td>三</td><td>叁</td></tr>
|
||||
</tbody>
|
||||
<tfoot><tr><td>尾</td><td>末</td></tr></tfoot>
|
||||
</table></main>
|
||||
`;
|
||||
const source = document.querySelector("#source")!;
|
||||
|
||||
expect(forcePagedTableRowsToNextPage(source, ["row-2"])).toBe(1);
|
||||
|
||||
const tables = source.querySelectorAll<HTMLTableElement>("table");
|
||||
expect(tables).toHaveLength(2);
|
||||
expect(Array.from(tables[0]!.querySelectorAll("tbody > tr")).map(
|
||||
(row) => (row as HTMLElement).dataset.mdtpTableRowId
|
||||
)).toEqual(["row-1"]);
|
||||
expect(Array.from(tables[1]!.querySelectorAll("tbody > tr")).map(
|
||||
(row) => (row as HTMLElement).dataset.mdtpTableRowId
|
||||
)).toEqual(["row-2", "row-3"]);
|
||||
expect(tables[1]!.dataset.mdtpTableRecoveryContinuation).toBe("true");
|
||||
expect(tables[1]!.querySelector("colgroup")).not.toBeNull();
|
||||
expect(tables[1]!.querySelector("thead")).not.toBeNull();
|
||||
expect(tables[0]!.querySelector("tfoot")).toBeNull();
|
||||
expect(tables[1]!.querySelector("tfoot")).not.toBeNull();
|
||||
const marker = tables[1]!.previousElementSibling as HTMLElement;
|
||||
expect(marker.dataset.mdtpTableRecoveryMarker).toBe("true");
|
||||
expect(marker.style.getPropertyValue("break-before")).toBe("page");
|
||||
expect(marker.style.getPropertyPriority("break-before")).toBe("important");
|
||||
});
|
||||
|
||||
it("仅将分页可视区域内真实可见的表格行视为已渲染", () => {
|
||||
document.body.innerHTML = `
|
||||
<main id="source"><table><tbody>
|
||||
<tr data-mdtp-table-row-id="row-clipped" data-mdtp-table-row-keep-whole="true"><td>裁切</td></tr>
|
||||
<tr data-mdtp-table-row-id="row-visible" data-mdtp-table-row-keep-whole="true"><td>可见</td></tr>
|
||||
</tbody></table></main>
|
||||
<main id="rendered">
|
||||
<section class="pagedjs_area" id="page-1">
|
||||
<table><tbody>
|
||||
<tr id="clipped" data-mdtp-table-row-id="row-clipped"><td>裁切</td></tr>
|
||||
<tr id="visible" data-mdtp-table-row-id="row-visible"><td>可见</td></tr>
|
||||
</tbody></table>
|
||||
</section>
|
||||
<section class="pagedjs_area" id="page-2">
|
||||
<table><tbody>
|
||||
<tr id="visible-duplicate" data-mdtp-table-row-id="row-visible"><td>可见副本</td></tr>
|
||||
</tbody></table>
|
||||
</section>
|
||||
</main>
|
||||
`;
|
||||
const rect = (
|
||||
left: number,
|
||||
top: number,
|
||||
right: number,
|
||||
bottom: number
|
||||
) => ({
|
||||
left, top, right, bottom,
|
||||
width: right - left,
|
||||
height: bottom - top,
|
||||
x: left,
|
||||
y: top,
|
||||
toJSON: () => ({})
|
||||
}) as DOMRect;
|
||||
document.querySelector<HTMLElement>("#page-1")!.getBoundingClientRect =
|
||||
() => rect(0, 0, 600, 800);
|
||||
document.querySelector<HTMLElement>("#page-2")!.getBoundingClientRect =
|
||||
() => rect(0, 900, 600, 1700);
|
||||
document.querySelector<HTMLElement>("#clipped")!.getBoundingClientRect =
|
||||
() => rect(0, 810, 600, 850);
|
||||
document.querySelector<HTMLElement>("#visible")!.getBoundingClientRect =
|
||||
() => rect(0, 780, 600, 820);
|
||||
document.querySelector<HTMLElement>("#visible-duplicate")!
|
||||
.getBoundingClientRect = () => rect(0, 920, 600, 960);
|
||||
|
||||
expect(missingPagedTableRowIds(
|
||||
document.querySelector("#source")!,
|
||||
document.querySelector("#rendered")!
|
||||
)).toEqual(["row-clipped"]);
|
||||
});
|
||||
|
||||
it("分页片段同步源表冻结列宽而不重复 colgroup", () => {
|
||||
document.body.innerHTML = `
|
||||
<table id="source" data-stabilized-columns="true">
|
||||
<colgroup data-stabilized-columns="true"><col data-stabilized-width-px="118" style="width: 120px"><col data-stabilized-width-px="282" style="width: 280px"></colgroup>
|
||||
<tbody><tr><td>甲</td><td>乙</td></tr></tbody>
|
||||
</table>
|
||||
<table id="rendered">
|
||||
<colgroup><col style="width: 100px"><col style="width: 300px"></colgroup>
|
||||
<tbody><tr><td>甲</td><td>乙</td></tr></tbody>
|
||||
</table>
|
||||
`;
|
||||
const source = document.querySelector<HTMLTableElement>("#source")!;
|
||||
const rendered = document.querySelector<HTMLTableElement>("#rendered")!;
|
||||
|
||||
restorePagedTableColumns(source, rendered);
|
||||
|
||||
expect(rendered.querySelectorAll(":scope > colgroup")).toHaveLength(1);
|
||||
expect(Array.from(rendered.querySelectorAll("col")).map(
|
||||
(column) => column.style.width
|
||||
)).toEqual(["120px", "280px"]);
|
||||
expect(Array.from(rendered.querySelectorAll("col")).map(
|
||||
(column) => (column as HTMLTableColElement).dataset.stabilizedWidthPx
|
||||
)).toEqual(["118", "282"]);
|
||||
expect(rendered.dataset.stabilizedColumns).toBe("true");
|
||||
expect(rendered.style.tableLayout).toBe("fixed");
|
||||
});
|
||||
|
||||
it("按真实上下文采集行内代码字号、颜色和盒模型", () => {
|
||||
document.body.innerHTML = `
|
||||
<article id="write" style="font-size: 20px">
|
||||
@@ -419,6 +956,42 @@ describe("DOCX 媒体捕获计划", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("不把普通两端对齐列表的自然末行误判为分散对齐", () => {
|
||||
document.body.innerHTML = `
|
||||
<article id="write">
|
||||
<ul><li style="text-align: justify; text-align-last: left">末行保持左对齐</li></ul>
|
||||
</article>
|
||||
`;
|
||||
const article = document.querySelector<HTMLElement>("#write")!;
|
||||
article.getBoundingClientRect = () => ({
|
||||
left: 0,
|
||||
right: 800,
|
||||
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) {
|
||||
return {
|
||||
left: this.startOffset * 80,
|
||||
right: this.startOffset * 80 + 10,
|
||||
top: 20,
|
||||
bottom: 32,
|
||||
width: 10,
|
||||
height: 12
|
||||
} as DOMRect;
|
||||
};
|
||||
try {
|
||||
expect(collectDocxListItemLayouts(article)[0]?.alignment).toBeUndefined();
|
||||
} finally {
|
||||
rangePrototype.getBoundingClientRect = original;
|
||||
}
|
||||
});
|
||||
|
||||
it("采集正文文本块在 Chromium 中的实际行断点", () => {
|
||||
document.body.innerHTML = `
|
||||
<article id="write">
|
||||
|
||||
@@ -40,6 +40,14 @@ const payload: PagedPreviewPayload = {
|
||||
};
|
||||
|
||||
describe("分页预览协议", () => {
|
||||
it("含行内代码的段落避免被 Paged.js 跨页拆分丢失片段", () => {
|
||||
const css = buildPagedMediaCss(defaultExportConfig, payload);
|
||||
|
||||
expect(css).toContain("#write > p.md-inline-code-paragraph");
|
||||
expect(css).toContain("orphans: 1");
|
||||
expect(css).toContain("widows: 1");
|
||||
});
|
||||
|
||||
it("生成真实纸张尺寸和页边距 CSS", () => {
|
||||
const css = buildPagedMediaCss(defaultExportConfig, payload);
|
||||
|
||||
@@ -278,6 +286,12 @@ describe("分页预览协议", () => {
|
||||
expect(documentBaseCss).toContain("white-space: pre-wrap;");
|
||||
expect(documentBaseCss).toContain("overflow-wrap: anywhere;");
|
||||
expect(documentBaseCss).toContain("word-break: break-word;");
|
||||
expect(documentBaseCss).toMatch(
|
||||
/#write th,[\s\S]*#write td \{[\s\S]*word-break: break-word;/u,
|
||||
);
|
||||
expect(documentBaseCss).toMatch(
|
||||
/#write th code,[\s\S]*#write td code \{[\s\S]*white-space: pre-wrap;[\s\S]*overflow-wrap: anywhere;[\s\S]*word-break: break-word;/u,
|
||||
);
|
||||
|
||||
const css = buildPagedMediaCss(defaultExportConfig, payload);
|
||||
expect(css).toContain("#write .md-code-line");
|
||||
|
||||
Reference in New Issue
Block a user