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( `` + `CONTAINER_START` + `可编辑封面` + `CONTAINER_END` + `SECTION_BREAK` + `正文标题` + `AB` + `` + `` ) ); entries.set( "word/footer1.xml", encoder.encode( ` NUMPAGES \\* MERGEFORMAT ` ) ); 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(/)/gu) ).toHaveLength(2); expect(documentXml).toContain( '' ); expect(documentXml).toContain( '' ); expect(documentXml).toContain( '' ); expect(documentXml).toContain( '' ); expect(documentXml).toContain( '' ); expect(documentXml).toContain(''); expect(documentXml).toContain(''); expect(documentXml).toContain(""); expect(documentXml).toContain('w:fill="F5F5F5"'); expect( documentXml.match(//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( "", '' + '' + '' + '' + '' + '' + "" ); 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( '' ); expect(outputStyles).not.toContain('w:val="justify"'); expect(outputStyles).not.toContain("