Files
MorphDoc/packages/preview-engine/tests/docx-media-runtime.test.ts
T
SkyJourney 64445322eb 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 均已生成并校验。
2026-08-26 10:50:20 +08:00

1040 lines
33 KiB
TypeScript

// @vitest-environment happy-dom
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
collectPagedDocxTableGeometries,
collectDocxDocumentLayoutPlan,
collectDocxInlineCodeLayouts,
collectDocxListItemLayouts,
collectDocxTextBlockLayouts,
collectDocxMediaCaptureTargets,
forcePagedTableRowsToNextPage,
mergePagedTableGeometries,
missingPagedTableRowIds,
protectFittableInlineCodeParagraphs,
removeInheritedPagedSplitJustification,
renderDocxMediaCapturePlan,
restorePagedTableColumns,
restorePagedTableStructure,
stabilizePagedTableColumns
} from "../src/index.js";
describe("DOCX 媒体捕获计划", () => {
beforeEach(() => {
document.body.innerHTML = "";
window.scrollTo(0, 0);
});
it("连续布局使用原始打印媒体主题 CSS", async () => {
const render = vi.fn(async () => ({
echartsErrors: [],
mermaidErrors: []
}));
const renderContinuous = vi.fn(async () => ({
echartsErrors: [],
mermaidErrors: []
}));
const root = document.createElement("main");
root.innerHTML = '<article id="write"></article>';
await renderDocxMediaCapturePlan(
{ render, 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" })
);
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 末行两端对齐继承给子块的容器标记", () => {
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">
<figure class="md-document-image-block">
<img class="md-document-image" alt="架构截图">
<figcaption>系统架构</figcaption>
</figure>
<div class="mermaid"><svg aria-label="处理流程"></svg></div>
<figure class="md-echarts">
<div class="md-echarts-host" aria-label="年度收入">
<svg></svg>
</div>
<figcaption>收入趋势</figcaption>
</figure>
</article>
`;
const media = Array.from(
document.querySelectorAll<HTMLElement | SVGSVGElement>(
"img, svg"
)
);
const article = document.querySelector<HTMLElement>("#write")!;
article.getBoundingClientRect = () =>
({
x: 0,
y: 0,
left: 0,
top: 0,
right: 700,
bottom: 900,
width: 700,
height: 900,
toJSON: () => ({})
}) as DOMRect;
const horizontalPositions = [0, 200, 400];
media.forEach((element, index) => {
const left = horizontalPositions[index]!;
element.getBoundingClientRect = () =>
({
x: left,
y: 20 + index * 100,
left,
top: 20 + index * 100,
right: left + 300,
bottom: 170 + index * 100,
width: 300,
height: 150,
toJSON: () => ({})
}) as DOMRect;
});
const targets = collectDocxMediaCaptureTargets(document, {
contentWidthPx: 700,
contentHeightPx: 900
});
expect(
targets.map(
({ id, kind, kindOrdinal, altText, caption, alignment }) => ({
id,
kind,
kindOrdinal,
altText,
caption,
alignment
})
)
).toEqual([
{
id: "docx-media-1",
kind: "image",
kindOrdinal: 1,
altText: "架构截图",
caption: "系统架构",
alignment: "left"
},
{
id: "docx-media-2",
kind: "mermaid",
kindOrdinal: 1,
altText: "处理流程",
caption: undefined,
alignment: "center"
},
{
id: "docx-media-3",
kind: "echarts",
kindOrdinal: 1,
altText: "年度收入",
caption: "收入趋势",
alignment: "right"
}
]);
expect(
media.map((element) => element.dataset.docxMediaId)
).toEqual(["docx-media-1", "docx-media-2", "docx-media-3"]);
expect(targets[0]?.rasterScale).toBe(3.125);
});
it("限制超大媒体的显示尺寸和 PNG 像素规模", () => {
document.body.innerHTML = `
<article id="write">
<img class="md-document-image" alt="">
</article>
`;
const image = document.querySelector("img")!;
const article = document.querySelector<HTMLElement>("#write")!;
article.getBoundingClientRect = () =>
({
x: 0,
y: 0,
left: 0,
top: 0,
right: 800,
bottom: 900,
width: 800,
height: 900,
toJSON: () => ({})
}) as DOMRect;
image.getBoundingClientRect = () =>
({
x: 0,
y: 0,
left: 0,
top: 0,
right: image.style.width ? 800 : 4000,
bottom: image.style.width ? 600 : 3000,
width: image.style.width ? 800 : 4000,
height: image.style.width ? 600 : 3000,
toJSON: () => ({})
}) as DOMRect;
const [target] = collectDocxMediaCaptureTargets(document, {
contentWidthPx: 800,
contentHeightPx: 900
});
expect(target?.displayWidthPx).toBeLessThanOrEqual(800);
expect(target?.displayHeightPx).toBeLessThanOrEqual(900);
expect(target?.alignment).toBe("center");
expect(
(target?.captureWidthPx ?? 0) *
(target?.rasterScale ?? 0)
).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"><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">
<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("不把普通两端对齐列表的自然末行误判为分散对齐", () => {
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">
<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;
}
});
});