feat: 完成 DOCX R4 元素级视觉门禁
建立 Chromium、Word 与 WPS 的可编辑内容、段落几何、页眉页脚、封面、媒体和逐页视觉比较链路。 支持封面独立分节、正文页码重启、主题页边距与横向纸张,并将自然分页差异降为诊断项。 修正代码块内边距和折叠表格单元格边框映射,14 套主题生产矩阵与六组布局视觉矩阵通过。
This commit is contained in:
@@ -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("年度 <报告>");
|
||||
expect(regularHeader).toContain("<w:bottom");
|
||||
expect(firstHeader).not.toContain("年度 <报告>");
|
||||
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(),
|
||||
|
||||
Reference in New Issue
Block a user