feat: 完成 DOCX R4 元素级视觉门禁

建立 Chromium、Word 与 WPS 的可编辑内容、段落几何、页眉页脚、封面、媒体和逐页视觉比较链路。

支持封面独立分节、正文页码重启、主题页边距与横向纸张,并将自然分页差异降为诊断项。

修正代码块内边距和折叠表格单元格边框映射,14 套主题生产矩阵与六组布局视觉矩阵通过。
This commit is contained in:
SkyJourney
2026-08-02 03:27:11 +08:00
parent f9f5fccfc9
commit 58f87cc19f
100 changed files with 10409 additions and 386 deletions
@@ -199,6 +199,10 @@ describe("动态 reference.docx", () => {
expect(documentXml).toContain('w:top="907"');
expect(stylesXml).toContain('w:styleId="SourceCode"');
expect(stylesXml).toContain('w:eastAsia="Microsoft YaHei"');
const imageCaptionStyle = stylesXml.match(
/<w:style\b[^>]*w:styleId="ImageCaption"[\s\S]*?<\/w:style>/u
)?.[0];
expect(imageCaptionStyle).toContain('<w:jc w:val="center"/>');
expect(footerXml).toContain(" PAGE \\* MERGEFORMAT ");
expect(footerXml).toContain(" NUMPAGES \\* MERGEFORMAT ");
expect(footerXml).toContain('w:jc w:val="center"');
@@ -226,7 +230,7 @@ describe("动态 reference.docx", () => {
"Source Han Serif SC",
"Times New Roman"
],
fontSizePt: 14,
fontSizePt: 12.75,
color: "#112233",
lineSpacing: 1.8,
firstLineIndentPt: 28,
@@ -246,6 +250,28 @@ describe("动态 reference.docx", () => {
keepWithNext: true
}
},
{
slot: "code-block",
source: "computed-css",
confidence: "exact",
style: {
fontCandidates: ["Lucida Console"],
fontSizePt: 9,
paddingPt: {
top: 6,
right: 3,
bottom: 5,
left: 3
},
contentPaddingLeftPt: 6,
borders: {
top: { widthPt: 0.75, color: "#e7eaed", style: "single" },
right: { widthPt: 0.75, color: "#e7eaed", style: "single" },
bottom: { widthPt: 0.75, color: "#e7eaed", style: "single" },
left: { widthPt: 0.75, color: "#e7eaed", style: "single" }
}
}
},
{
slot: "table",
source: "computed-css",
@@ -268,6 +294,36 @@ describe("动态 reference.docx", () => {
}
}
},
{
slot: "table-cell",
source: "computed-css",
confidence: "exact",
style: {
fontCandidates: ["SimSun"],
borders: {
top: {
widthPt: 0.85,
color: "#556677",
style: "single"
},
right: {
widthPt: 0.85,
color: "#556677",
style: "single"
},
bottom: {
widthPt: 0.85,
color: "#556677",
style: "single"
},
left: {
widthPt: 0.85,
color: "#556677",
style: "single"
}
}
}
},
{
slot: "official-title",
source: "computed-css",
@@ -288,6 +344,20 @@ describe("动态 reference.docx", () => {
fontCandidates: [],
lineSpacing: 1.5
}
},
{
slot: "official-printing-row",
source: "computed-css",
confidence: "exact",
style: {
fontCandidates: [],
paddingPt: {
top: 3,
right: 28,
bottom: 3,
left: 28
}
}
}
])
}
@@ -302,22 +372,46 @@ describe("动态 reference.docx", () => {
);
expect(stylesXml).toContain('w:styleId="MdOfficialTitle"');
expect(stylesXml).toMatch(
/w:style[^>]*w:styleId="MdOfficialTitle"[\s\S]*?<w:autoSpaceDE w:val="0"\/><w:autoSpaceDN w:val="0"\/>/u
);
expect(stylesXml).toContain('w:eastAsia="Source Han Serif SC"');
expect(stylesXml).toContain('w:ascii="Times New Roman"');
expect(stylesXml).toContain('w:val="112233"');
expect(stylesXml).toContain('w:styleId="Heading1"');
expect(stylesXml).toContain('w:val="AA0000"');
expect(stylesXml).toContain('<w:jc w:val="both"/>');
const sourceCodeStyle = stylesXml.match(
/<w:style\b[^>]*w:styleId="SourceCode"[\s\S]*?<\/w:style>/u
)?.[0];
expect(sourceCodeStyle).toContain(
'<w:left w:val="single" w:sz="6" w:space="7" w:color="E7EAED"/>'
);
expect(sourceCodeStyle).toMatch(
/<w:ind\b[^>]*w:left="195"[^>]*w:right="75"[^>]*\/>/u
);
expect(stylesXml).not.toContain('w:val="justify"');
expect(stylesXml).toContain(
'w:line="504" w:lineRule="exact"'
'w:line="459" w:lineRule="exact"'
);
expect(stylesXml).toMatch(
/w:style[^>]*w:styleId="Normal"[\s\S]*?<w:rPr>[\s\S]*?<w:sz w:val="26"\/>[\s\S]*?<w:spacing w:val="-5"\/>/u
);
expect(stylesXml).toMatch(
/w:style[^>]*w:styleId="MdOfficialEdition"[\s\S]*?<w:spacing[^>]*w:line="315"[^>]*w:lineRule="exact"/u
);
expect(stylesXml).toMatch(
/w:style[^>]*w:styleId="MdOfficialPrintingRow"[\s\S]*?<w:spacing[^>]*w:before="60"[^>]*w:after="60"/u
);
expect(stylesXml).toMatch(
/w:style[^>]*w:styleId="MdOfficialPrintingRow"[\s\S]*?<w:ind[^>]*w:left="560"[^>]*w:right="560"/u
);
expect(stylesXml).not.toContain('w:lineRule="auto"');
expect(stylesXml).not.toContain("<w:tblW");
expect(stylesXml).toContain('w:color="445566"');
expect(stylesXml).toMatch(
/<w:tblBorders>[\s\S]*?<w:insideH[^>]*w:sz="7"[^>]*w:color="556677"\/>[\s\S]*?<w:insideV[^>]*w:sz="7"[^>]*w:color="556677"\/>/u
);
expect(fontTableXml).toContain(
'w:name="Source Han Serif SC"'
);
@@ -377,6 +471,8 @@ describe("动态 reference.docx", () => {
expect(documentXml).toContain('w:orient="landscape"');
expect(documentXml).toContain('w:w="16838"');
expect(documentXml).toContain('w:left="1701"');
expect(documentXml).toContain('w:header="723"');
expect(documentXml).toContain('w:footer="751"');
expect(documentXml).toContain('w:start="5"');
expect(documentXml).toContain("<w:titlePg");
expect(documentXml.match(/w:headerReference/gu)).toHaveLength(3);
@@ -390,10 +486,15 @@ describe("动态 reference.docx", () => {
expect(headerXml).toContain('w:pos="13720"');
expect(headerXml).toContain("<w:pBdr>");
expect(headerXml).toContain("<w:bottom");
expect(headerXml).toMatch(
/<w:spacing[^>]*w:line="213"[^>]*w:lineRule="exact"/u
);
expect(headerXml).not.toContain("<w:tbl");
expect(
decoder.decode(entries["word/footer1.xml"])
).not.toContain("<w:tbl");
const footerXml = decoder.decode(entries["word/footer1.xml"]);
expect(footerXml).toMatch(
/<w:spacing[^>]*w:line="213"[^>]*w:lineRule="exact"/u
);
expect(footerXml).not.toContain("<w:tbl");
expect(relationships.match(/relationships\/header/gu)).toHaveLength(
3
);
@@ -407,6 +508,133 @@ describe("动态 reference.docx", () => {
});
});
it("封面页码重启时仍使用原生奇偶页脚保持跨客户端外侧位置", () => {
const exportConfig: ExportConfig = {
...defaultExportConfig,
pageDecorationsMode: "custom",
footer: {
...defaultExportConfig.footer,
alignment: "outer",
format: "page-total",
startFrom: 1
}
};
const result = createDynamicReferenceDocx(
createBaselineReference(),
{
...createOptions(exportConfig),
semanticDocument: {
schemaVersion: 1,
profile: "project-report",
titlePolicy: {
metadataTitle: "suppress",
firstBodyHeading: "keep"
},
regions: [
{
kind: "cover",
nodes: [
{
kind: "text",
role: "project-report-title",
text: "可研报告"
}
],
section: {
headerFooter: "none",
pageNumber: "hidden",
breakAfter: "next-page",
followingPageNumberStart: 1
}
}
]
}
}
);
const entries = unzipSync(result.content);
const documentXml = decoder.decode(entries["word/document.xml"]);
const settingsXml = decoder.decode(entries["word/settings.xml"]);
const defaultFooterXml = decoder.decode(entries["word/footer1.xml"]);
const evenFooterXml = decoder.decode(entries["word/footer2.xml"]);
expect(documentXml.match(/w:footerReference/gu)).toHaveLength(2);
expect(settingsXml).toContain("<w:evenAndOddHeaders");
expect(defaultFooterXml).toContain('w:jc w:val="left"');
expect(evenFooterXml).toContain('w:jc w:val="right"');
expect(defaultFooterXml).toContain(" = ");
expect(defaultFooterXml).toContain(" - 1 ");
expect(defaultFooterXml).toContain(" PAGE \\* MERGEFORMAT ");
expect(defaultFooterXml).toContain(" NUMPAGES \\* MERGEFORMAT ");
expect(defaultFooterXml).not.toContain(" IF ");
expect(defaultFooterXml).not.toContain("<w:tbl");
expect(validateDynamicReferenceDocx(result.content)).toMatchObject({
footerCount: 2
});
});
it("独立控制正文首页页眉和页码", () => {
const exportConfig: ExportConfig = {
...defaultExportConfig,
pageDecorationsMode: "custom",
header: {
...defaultExportConfig.header,
enabled: true,
showOnFirstPage: false,
showDivider: true,
center: {
enabled: true,
content: "${title}"
}
},
footer: {
...defaultExportConfig.footer,
alignment: "center",
showOnFirstPage: true
}
};
const result = createDynamicReferenceDocx(
createBaselineReference(),
createOptions(exportConfig)
);
const entries = unzipSync(result.content);
const documentXml = decoder.decode(entries["word/document.xml"]);
const regularHeader = decoder.decode(entries["word/header1.xml"]);
const firstHeader = decoder.decode(entries["word/header2.xml"]);
const regularFooter = decoder.decode(entries["word/footer1.xml"]);
const firstFooter = decoder.decode(entries["word/footer2.xml"]);
expect(documentXml).toContain("<w:titlePg");
expect(documentXml.match(/w:headerReference/gu)).toHaveLength(2);
expect(documentXml.match(/w:footerReference/gu)).toHaveLength(2);
expect(regularHeader).toContain("年度 &lt;报告&gt;");
expect(regularHeader).toContain("<w:bottom");
expect(firstHeader).not.toContain("年度 &lt;报告&gt;");
expect(firstHeader).not.toContain("<w:bottom");
expect(regularFooter).toContain("PAGE");
expect(firstFooter).toContain("PAGE");
});
it("隐藏正文首页页码时生成无内容且无分隔线的首页页脚", () => {
const exportConfig: ExportConfig = {
...defaultExportConfig,
pageDecorationsMode: "custom",
footer: {
...defaultExportConfig.footer,
showDivider: true,
showOnFirstPage: false
}
};
const result = createDynamicReferenceDocx(
createBaselineReference(),
createOptions(exportConfig)
);
const entries = unzipSync(result.content);
const firstFooter = decoder.decode(entries["word/footer2.xml"]);
expect(firstFooter).not.toContain("PAGE");
expect(firstFooter).not.toContain("<w:top");
});
it("拒绝正文中断裂或类型错误的页眉页脚关系", () => {
const result = createDynamicReferenceDocx(
createBaselineReference(),