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
);
});
});