276 lines
8.8 KiB
TypeScript
276 lines
8.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { unzipSync, zipSync } from "fflate";
|
|
import {
|
|
defaultExportConfig,
|
|
type ExportConfig,
|
|
type ThemeManifest
|
|
} from "@md-to-pdf/core";
|
|
import {
|
|
createDynamicReferenceDocx,
|
|
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 `<w:style w:type="${type}" w:styleId="${styleId}"><w:name w:val="${styleId}"/></w:style>`;
|
|
})
|
|
.join("");
|
|
|
|
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"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>'
|
|
),
|
|
"_rels/.rels": xml(
|
|
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"/>'
|
|
),
|
|
"word/document.xml": xml(
|
|
`<w:document xmlns:w="${wordNamespace}" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><w:body><w:p/><w:sectPr><w:footnotePr/></w:sectPr></w:body></w:document>`
|
|
),
|
|
"word/fontTable.xml": xml(
|
|
`<w:fonts xmlns:w="${wordNamespace}"><w:font w:name="Arial"/></w:fonts>`
|
|
),
|
|
"word/numbering.xml": xml(
|
|
`<w:numbering xmlns:w="${wordNamespace}"/>`
|
|
),
|
|
"word/settings.xml": xml(
|
|
`<w:settings xmlns:w="${wordNamespace}"/>`
|
|
),
|
|
"word/styles.xml": xml(
|
|
`<w:styles xmlns:w="${wordNamespace}"><w:docDefaults><w:rPrDefault><w:rPr/></w:rPrDefault><w:pPrDefault><w:pPr/></w:pPrDefault></w:docDefaults>${styles}</w:styles>`
|
|
),
|
|
"word/_rels/document.xml.rels": xml(
|
|
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>'
|
|
),
|
|
"word/theme/theme1.xml": xml(
|
|
'<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:themeElements><a:fontScheme><a:majorFont><a:latin typeface="Arial"/><a:ea typeface=""/></a:majorFont><a:minorFont><a:latin typeface="Arial"/><a:ea typeface=""/></a:minorFont></a:fontScheme></a:themeElements></a:theme>'
|
|
)
|
|
},
|
|
{ mtime: new Date("2020-01-01T00:00:00.000Z") }
|
|
);
|
|
}
|
|
|
|
function theme(
|
|
overrides: Partial<ThemeManifest> = {}
|
|
): 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(validateDynamicReferenceDocx(first.content)).toMatchObject({
|
|
partCount: first.partCount,
|
|
headerCount: 0,
|
|
footerCount: 1
|
|
});
|
|
});
|
|
|
|
it("生成横向、自定义页边距和奇偶首页页眉页脚", () => {
|
|
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("<w:titlePg");
|
|
expect(documentXml.match(/w:headerReference/gu)).toHaveLength(3);
|
|
expect(documentXml.match(/w:footerReference/gu)).toHaveLength(3);
|
|
expect(settingsXml).toContain("<w:evenAndOddHeaders");
|
|
expect(headerXml).toContain("年度 <报告>");
|
|
expect(headerXml).toContain("报告 & 计划.md");
|
|
expect(relationships.match(/relationships\/header/gu)).toHaveLength(
|
|
3
|
|
);
|
|
expect(relationships.match(/relationships\/footer/gu)).toHaveLength(
|
|
3
|
|
);
|
|
expect(entries["word/footer3.xml"]).toBeDefined();
|
|
expect(validateDynamicReferenceDocx(result.content)).toMatchObject({
|
|
headerCount: 3,
|
|
footerCount: 3
|
|
});
|
|
});
|
|
|
|
it("拒绝正文中断裂或类型错误的页眉页脚关系", () => {
|
|
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
|
|
);
|
|
});
|
|
});
|