Files
MorphDoc/packages/docx-engine/tests/document-structure-transform.test.ts
T

252 lines
7.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { DocxThemeTokenSet } from "@md-to-pdf/docx-theme-engine";
import {
finalizeGeneratedDocxStructure,
readGeneratedDocxPackage,
readReferenceDocxPackage,
writeGeneratedDocxPackage,
type PandocStructurePlan
} from "../src/index.js";
import { createTestBaselineReference } from "./reference-test-fixture.js";
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const word =
"http://schemas.openxmlformats.org/wordprocessingml/2006/main";
const relationships =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships";
function generatedFixture() {
const baseline = readReferenceDocxPackage(
createTestBaselineReference()
);
const entries = new Map(baseline.entries);
entries.set(
"word/document.xml",
encoder.encode(
`<w:document xmlns:w="${word}" xmlns:r="${relationships}"><w:body>` +
`<w:p><w:r><w:t>CONTAINER_START</w:t></w:r></w:p>` +
`<w:p><w:pPr><w:pStyle w:val="MdTenderTitle"/></w:pPr><w:r><w:t>可编辑封面</w:t></w:r></w:p>` +
`<w:p><w:r><w:t>CONTAINER_END</w:t></w:r></w:p>` +
`<w:p><w:r><w:t>SECTION_BREAK</w:t></w:r></w:p>` +
`<w:p><w:pPr><w:pStyle w:val="Heading1"/></w:pPr><w:r><w:t>正文标题</w:t></w:r></w:p>` +
`<w:tbl><w:tblPr><w:tblW w:w="0" w:type="auto"/></w:tblPr><w:tblGrid><w:gridCol w:w="1000"/><w:gridCol w:w="2000"/></w:tblGrid><w:tr><w:tc><w:tcPr/><w:p><w:r><w:t>A</w:t></w:r></w:p></w:tc><w:tc><w:tcPr/><w:p><w:r><w:t>B</w:t></w:r></w:p></w:tc></w:tr></w:tbl>` +
`<w:sectPr><w:footerReference w:type="default" r:id="rIdFooter"/><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="1000" w:right="1000" w:bottom="1000" w:left="1000"/><w:pgNumType w:start="5"/><w:titlePg/></w:sectPr>` +
`</w:body></w:document>`
)
);
entries.set(
"word/footer1.xml",
encoder.encode(
`<w:ftr xmlns:w="${word}"><w:p><w:r><w:instrText xml:space="preserve"> NUMPAGES \\* MERGEFORMAT </w:instrText></w:r></w:p></w:ftr>`
)
);
return writeGeneratedDocxPackage(entries);
}
const plan: PandocStructurePlan = {
schemaVersion: 1,
titlePolicy: {
metadataTitle: "suppress",
firstBodyHeading: "keep"
},
prefix: [
{
kind: "container",
styleId: "MdTenderCover",
slot: "tender-cover",
startMarker: "CONTAINER_START",
endMarker: "CONTAINER_END",
blocks: []
},
{
kind: "section-break",
marker: "SECTION_BREAK",
headerFooter: "none",
pageNumber: "hidden",
followingPageNumberStart: 1,
verticalAlignment: "center"
}
],
suffix: []
};
const tokens: DocxThemeTokenSet = {
schemaVersion: 1,
themeId: "test-theme",
themeFingerprint: "c".repeat(64),
mode: "auto-with-overrides",
basePreset: "tender",
slots: [
{
slot: "tender-cover",
source: "computed-css",
confidence: "approximate",
style: {
fontCandidates: [],
backgroundColor: "#f5f5f5",
pageBreakAfter: true,
keepLines: true,
borders: {
top: {
widthPt: 1,
style: "single",
color: "#111111"
},
right: {
widthPt: 1,
style: "single",
color: "#111111"
},
bottom: {
widthPt: 1,
style: "single",
color: "#111111"
},
left: {
widthPt: 1,
style: "single",
color: "#111111"
}
}
}
},
{
slot: "table",
source: "computed-css",
confidence: "exact",
style: {
fontCandidates: [],
widthPercent: 100,
keepLines: true
}
},
{
slot: "heading-1",
source: "computed-css",
confidence: "exact",
style: {
fontCandidates: [],
pageBreakAfter: true
}
}
],
diagnostics: []
};
describe("生成 DOCX 结构收口", () => {
it("生成真实分节、正文页码、节总页数和固定宽度表格", () => {
const result = finalizeGeneratedDocxStructure(
generatedFixture(),
plan,
tokens
);
const entries = readGeneratedDocxPackage(
result.content
).entries;
const documentXml = decoder.decode(
entries.get("word/document.xml")!
);
const footerXml = decoder.decode(
entries.get("word/footer1.xml")!
);
expect(documentXml).not.toContain("CONTAINER_START");
expect(documentXml).not.toContain("CONTAINER_END");
expect(documentXml).not.toContain("SECTION_BREAK");
expect(
documentXml.match(/<w:sectPr(?:\s|>)/gu)
).toHaveLength(2);
expect(documentXml).toContain(
'<w:type w:val="nextPage"/>'
);
expect(documentXml).toContain(
'<w:vAlign w:val="center"/>'
);
expect(documentXml).toContain(
'<w:pgNumType w:start="1"/>'
);
expect(documentXml).toContain(
'<w:tblW w:w="5000" w:type="pct"/>'
);
expect(documentXml).toContain(
'<w:tblLayout w:type="fixed"/>'
);
expect(documentXml).toContain('<w:gridCol w:w="3302"/>');
expect(documentXml).toContain('<w:gridCol w:w="6604"/>');
expect(documentXml).toContain("<w:cantSplit/>");
expect(documentXml).toContain('w:fill="F5F5F5"');
expect(
documentXml.match(/<w:br w:type="page"\/>/gu)
).toHaveLength(1);
expect(footerXml).toContain("SECTIONPAGES");
expect(footerXml).not.toContain(" NUMPAGES ");
expect(result.report).toEqual({
containerCount: 1,
sectionCount: 1,
tableCount: 1,
pageBreakAfterCount: 1,
sectionPageFieldCount: 1
});
});
it("拒绝缺失或重复的内部结构标记", () => {
expect(() =>
finalizeGeneratedDocxStructure(
generatedFixture(),
{
...plan,
prefix: [
{
...plan.prefix[0]!,
startMarker: "MISSING"
},
plan.prefix[1]!
]
},
tokens
)
).toThrow("结构标记数量无效");
});
it("清理 Pandoc 追加的重复和降级样式", () => {
const source = readGeneratedDocxPackage(
generatedFixture()
);
const entries = new Map(source.entries);
const styles = decoder
.decode(entries.get("word/styles.xml")!)
.replace(
"</w:styles>",
'<w:style w:type="paragraph" w:styleId="MdTenderTitle">' +
'<w:name w:val="重复降级样式"/></w:style>' +
'<w:style w:type="paragraph" w:styleId="PandocFallback">' +
'<w:pPr><w:jc w:val="justify"/></w:pPr></w:style>' +
'<w:style w:type="table" w:styleId="PandocTableFallback">' +
'<w:tblPr><w:tblW w:w="5000" w:type="pct"/></w:tblPr>' +
"</w:style></w:styles>"
);
entries.set("word/styles.xml", encoder.encode(styles));
const result = finalizeGeneratedDocxStructure(
writeGeneratedDocxPackage(entries),
plan,
tokens
);
const outputStyles = decoder.decode(
readGeneratedDocxPackage(result.content).entries.get(
"word/styles.xml"
)!
);
expect(
outputStyles.match(/w:styleId="MdTenderTitle"/gu)
).toHaveLength(1);
expect(outputStyles).toContain(
'<w:jc w:val="both"/>'
);
expect(outputStyles).not.toContain('w:val="justify"');
expect(outputStyles).not.toContain("<w:tblW");
});
});