import { describe, expect, it } from "vitest"; import { randomBytes } from "node:crypto"; import { unzipSync, zipSync } from "fflate"; import { defaultExportConfig, type ExportConfig, type ThemeManifest } from "@md-to-pdf/core"; import { createDynamicReferenceDocx, validateGeneratedDocx, validateDynamicReferenceDocx, writeReferenceDocxPackage } from "../src/index.js"; const encoder = new TextEncoder(); const decoder = new TextDecoder(); const wordNamespace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; function xml(value: string) { return encoder.encode(value); } function createBaselineReference() { const styles = [ "Normal", "BodyText", "FirstParagraph", "Compact", "Title", "TitleChar", "Subtitle", "SubtitleChar", "Heading1", "Heading1Char", "Heading2", "Heading2Char", "Heading3", "Heading3Char", "Heading4", "Heading4Char", "Heading5", "Heading5Char", "Heading6", "Heading6Char", "BlockText", "FootnoteText", "DefaultParagraphFont", "Table", "Definition", "Caption", "TableCaption", "ImageCaption", "Figure", "BodyTextChar", "VerbatimChar", "Hyperlink" ] .map((styleId) => { const type = styleId === "Table" ? "table" : styleId.endsWith("Char") || styleId === "DefaultParagraphFont" || styleId === "VerbatimChar" || styleId === "Hyperlink" ? "character" : "paragraph"; return ``; }) .join(""); return zipSync( { "[Content_Types].xml": xml( '' ), "_rels/.rels": xml( '' ), "word/document.xml": xml( `` ), "word/fontTable.xml": xml( `` ), "word/numbering.xml": xml( `` ), "word/settings.xml": xml( `` ), "word/styles.xml": xml( `${styles}` ), "word/_rels/document.xml.rels": xml( '' ), "word/theme/theme1.xml": xml( '' ) }, { mtime: new Date("2020-01-01T00:00:00.000Z") } ); } function theme( overrides: Partial = {} ): ThemeManifest { return { manifestVersion: 1, id: "test-theme", name: "测试主题", version: "1.0.0", description: "测试", author: "测试", license: "内部许可", entry: "theme.css", domPreset: "generic", defaultFontSize: "16px", supportedFeatures: [], category: "general", compatibleProfiles: [], docxStyle: { preset: "technical" }, bundled: true, ...overrides }; } function createOptions(exportConfig: ExportConfig) { return { exportConfig, theme: theme(), fileName: "报告 & 计划.md", metadata: { title: "年度 <报告>", author: "测试人" } }; } describe("动态 reference.docx", () => { it("生成稳定的纸张、正文样式和页码模板", () => { const baseline = createBaselineReference(); const first = createDynamicReferenceDocx( baseline, createOptions(defaultExportConfig) ); const second = createDynamicReferenceDocx( baseline, createOptions(defaultExportConfig) ); const changedMetadata = createDynamicReferenceDocx(baseline, { ...createOptions(defaultExportConfig), metadata: { title: "另一份报告", author: "测试人" } }); const entries = unzipSync(first.content); const documentXml = decoder.decode(entries["word/document.xml"]); const stylesXml = decoder.decode(entries["word/styles.xml"]); const footerXml = decoder.decode(entries["word/footer1.xml"]); expect(first.templateFingerprint).toBe( second.templateFingerprint ); expect(first.cacheKey).toBe(second.cacheKey); expect(first.cacheKey).not.toBe(changedMetadata.cacheKey); expect(documentXml).toContain('w:w="11906"'); expect(documentXml).toContain('w:h="16838"'); expect(documentXml).toContain('w:top="907"'); expect(stylesXml).toContain('w:styleId="SourceCode"'); expect(stylesXml).toContain('w:eastAsia="Microsoft YaHei"'); expect(footerXml).toContain(" PAGE \\* MERGEFORMAT "); expect(footerXml).toContain(" NUMPAGES \\* MERGEFORMAT "); expect(footerXml).toContain('w:jc w:val="center"'); expect(footerXml).toContain('w:before="0"'); expect(footerXml).not.toContain(" { const exportConfig: ExportConfig = { ...defaultExportConfig, pageDecorationsMode: "custom", paper: { ...defaultExportConfig.paper, orientation: "landscape", marginMode: "custom", margins: { top: "20mm", right: "25mm", bottom: "22mm", left: "30mm" } }, header: { ...defaultExportConfig.header, enabled: true, showDivider: true, left: { enabled: true, content: "${title} / ${filename}" } }, footer: { ...defaultExportConfig.footer, alignment: "outer", format: "official-page", startFrom: 5, showOnFirstPage: false } }; const result = createDynamicReferenceDocx( createBaselineReference(), createOptions(exportConfig) ); const entries = unzipSync(result.content); const documentXml = decoder.decode(entries["word/document.xml"]); const settingsXml = decoder.decode(entries["word/settings.xml"]); const headerXml = decoder.decode(entries["word/header1.xml"]); const relationships = decoder.decode( entries["word/_rels/document.xml.rels"] ); expect(documentXml).toContain('w:orient="landscape"'); expect(documentXml).toContain('w:w="16838"'); expect(documentXml).toContain('w:left="1701"'); expect(documentXml).toContain('w:start="5"'); expect(documentXml).toContain(""); expect(headerXml).toContain(" { const result = createDynamicReferenceDocx( createBaselineReference(), createOptions(defaultExportConfig) ); const entries = new Map( Object.entries(unzipSync(result.content)) ); const relationships = decoder .decode(entries.get("word/_rels/document.xml.rels")!) .replace( "relationships/footer", "relationships/header" ); entries.set( "word/_rels/document.xml.rels", encoder.encode(relationships) ); const damaged = writeReferenceDocxPackage(entries); expect(() => validateDynamicReferenceDocx(damaged)).toThrow( /footerReference 引用了无效关系/u ); }); it("拒绝使用表格模拟页眉页脚布局", () => { const result = createDynamicReferenceDocx( createBaselineReference(), createOptions(defaultExportConfig) ); const entries = new Map( Object.entries(unzipSync(result.content)) ); const footer = decoder .decode(entries.get("word/footer1.xml")!) .replace( "", "" ); entries.set("word/footer1.xml", encoder.encode(footer)); const damaged = writeReferenceDocxPackage(entries); expect(() => validateDynamicReferenceDocx(damaged)).toThrow( /不得使用表格模拟页眉页脚布局/u ); }); it("最终 DOCX 使用媒体输出上限而非模板的 2 MiB 上限", () => { const result = createDynamicReferenceDocx( createBaselineReference(), createOptions(defaultExportConfig) ); const entries = unzipSync(result.content); const generated = zipSync( { ...entries, "word/media/large-test.bin": randomBytes(3 * 1024 * 1024) }, { level: 0 } ); expect(generated.byteLength).toBeGreaterThan(2 * 1024 * 1024); expect(validateGeneratedDocx(generated).partCount).toBe( result.partCount + 1 ); expect(() => validateDynamicReferenceDocx(generated)).toThrow( /大小超过限制/u ); }); });