252 lines
7.3 KiB
TypeScript
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");
|
|
});
|
|
});
|