feat: 完成 DOCX 通用结构映射与严格收口

This commit is contained in:
SkyJourney
2026-07-31 15:19:34 +08:00
parent 6e40374200
commit f3847f3e4b
31 changed files with 3175 additions and 64 deletions
@@ -32,7 +32,14 @@ function xml(value: string) {
return encoder.encode(value);
}
function createAcceptanceDocx(overrides: { altChunk?: boolean } = {}) {
function createAcceptanceDocx(
overrides: {
altChunk?: boolean;
duplicateText?: boolean;
internalMarker?: boolean;
invalidPropertyOrder?: boolean;
} = {}
) {
return zipSync({
"[Content_Types].xml": xml(
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="xml" ContentType="application/xml"/><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="png" ContentType="image/png"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/><Override PartName="/word/footer1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"/></Types>'
@@ -41,7 +48,7 @@ function createAcceptanceDocx(overrides: { altChunk?: boolean } = {}) {
`<Relationships xmlns="${packageRelationships}"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>`
),
"word/document.xml": xml(
`<w:document xmlns:w="${word}" xmlns:r="${relationships}" xmlns:m="${math}" xmlns:wp="${drawing}"><w:body><w:p><w:pPr><w:numPr><w:numId w:val="1"/></w:numPr></w:pPr><w:hyperlink r:id="rIdLink"><w:r><w:t>可编辑正文</w:t></w:r></w:hyperlink><w:r><w:footnoteReference w:id="1"/></w:r><m:oMath><m:r><m:t>x</m:t></m:r></m:oMath></w:p><w:tbl><w:tr><w:tc><w:p><w:r><w:t>原生表格</w:t></w:r></w:p></w:tc></w:tr></w:tbl><w:p><w:r><w:drawing><wp:inline><wp:docPr id="1" name="图片 1" descr="普通图片"/><a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"/></wp:inline></w:drawing></w:r></w:p>${overrides.altChunk ? '<w:altChunk r:id="rIdChunk"/>' : ""}<w:sectPr><w:footerReference w:type="default" r:id="rIdFooter"/><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="907" w:right="907" w:bottom="907" w:left="907"/></w:sectPr></w:body></w:document>`
`<w:document xmlns:w="${word}" xmlns:r="${relationships}" xmlns:m="${math}" xmlns:wp="${drawing}"><w:body><w:p><w:pPr><w:pStyle w:val="Heading1"/><w:numPr><w:numId w:val="1"/></w:numPr></w:pPr><w:hyperlink r:id="rIdLink"><w:r><w:t>可编辑正文</w:t></w:r></w:hyperlink><w:r><w:footnoteReference w:id="1"/></w:r><m:oMath><m:r><m:t>x</m:t></m:r></m:oMath></w:p>${overrides.invalidPropertyOrder ? '<w:p><w:pPr><w:jc w:val="left"/><w:spacing w:after="0"/></w:pPr></w:p>' : ""}${overrides.duplicateText ? "<w:p><w:r><w:t>可编辑正文</w:t></w:r></w:p>" : ""}${overrides.internalMarker ? "<w:p><w:r><w:t>MD_TO_PDF_INTERNAL</w:t></w:r></w:p>" : ""}<w:p><w:pPr><w:sectPr><w:type w:val="nextPage"/><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="907" w:right="907" w:bottom="907" w:left="907"/></w:sectPr></w:pPr></w:p><w:tbl><w:tblPr><w:tblW w:w="5000" w:type="pct"/></w:tblPr><w:tr><w:tc><w:p><w:r><w:t>原生表格</w:t></w:r></w:p></w:tc></w:tr></w:tbl><w:p><w:r><w:drawing><wp:inline><wp:docPr id="1" name="图片 1" descr="普通图片"/><a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"/></wp:inline></w:drawing></w:r></w:p>${overrides.altChunk ? '<w:altChunk r:id="rIdChunk"/>' : ""}<w:sectPr><w:footerReference w:type="default" r:id="rIdFooter"/><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="907" w:right="907" w:bottom="907" w:left="907"/><w:pgNumType w:start="1"/></w:sectPr></w:body></w:document>`
),
"word/styles.xml": xml(
`<w:styles xmlns:w="${word}">${["Normal", "Heading1", "SourceCode", "Table", "Caption"].map((id) => `<w:style w:type="paragraph" w:styleId="${id}"/>`).join("")}</w:styles>`
@@ -53,7 +60,7 @@ function createAcceptanceDocx(overrides: { altChunk?: boolean } = {}) {
'<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"/>'
),
"word/footer1.xml": xml(
`<w:ftr xmlns:w="${word}"><w:p><w:r><w:instrText> PAGE \\* MERGEFORMAT </w:instrText></w:r></w:p></w:ftr>`
`<w:ftr xmlns:w="${word}"><w:p><w:r><w:instrText> PAGE \\* MERGEFORMAT </w:instrText></w:r><w:r><w:instrText> SECTIONPAGES \\* MERGEFORMAT </w:instrText></w:r></w:p></w:ftr>`
),
"word/footnotes.xml": xml(
`<w:footnotes xmlns:w="${word}"><w:footnote w:id="1"><w:p><w:r><w:t>脚注</w:t></w:r></w:p></w:footnote></w:footnotes>`
@@ -93,7 +100,17 @@ const expectation = {
minimumPngImages: 1,
requiredImageAltText: ["普通图片"],
requiredStyleIds: ["Normal", "Heading1", "SourceCode", "Table"],
requirePageField: true
requiredParagraphStyleIds: ["Heading1"],
requirePageField: true,
minimumSections: 2,
requireFirstSectionWithoutHeaderFooter: true,
finalPageNumberStart: 1,
requireSectionPagesField: true,
minimumFullWidthTables: 1,
maximumTextOccurrences: {
可编辑正文: 1
},
forbidInternalMarkers: true
};
describe("DOCX 自动验收器", () => {
@@ -112,6 +129,10 @@ describe("DOCX 自动验收器", () => {
mathObjectCount: 1,
pngImageCount: 1,
pageFieldCount: 1,
sectionPageFieldCount: 1,
sectionCount: 2,
fullWidthTableCount: 1,
internalMarkerCount: 0,
altChunkCount: 0
});
expect(report.checks).toEqual(
@@ -131,4 +152,28 @@ describe("DOCX 自动验收器", () => {
)
).toThrow("altChunk");
});
it("拒绝重复文本和未清理的内部标记", () => {
expect(() =>
inspectDocxAcceptance(
createAcceptanceDocx({ duplicateText: true }),
expectation
)
).toThrow("最多允许 1 次");
expect(() =>
inspectDocxAcceptance(
createAcceptanceDocx({ internalMarker: true }),
expectation
)
).toThrow("internal-markers");
});
it("拒绝 WordprocessingML 属性子节点乱序", () => {
expect(() =>
inspectDocxAcceptance(
createAcceptanceDocx({ invalidPropertyOrder: true }),
expectation
)
).toThrow("w:pPr 子节点顺序无效");
});
});