feat: 将主题令牌接入 DOCX 动态模板

This commit is contained in:
SkyJourney
2026-07-31 08:43:26 +08:00
parent 459a079f3b
commit 6e40374200
33 changed files with 1478 additions and 56 deletions
@@ -6,6 +6,10 @@ import {
type ExportConfig,
type ThemeManifest
} from "@md-to-pdf/core";
import type {
DocxResolvedStyleSlot,
DocxThemeTokenSet
} from "@md-to-pdf/docx-theme-engine";
import {
createDynamicReferenceDocx,
validateGeneratedDocx,
@@ -129,6 +133,21 @@ function theme(
};
}
function themeTokens(
fingerprint = "c".repeat(64),
slots: DocxResolvedStyleSlot[] = []
): DocxThemeTokenSet {
return {
schemaVersion: 1,
themeId: "test-theme",
themeFingerprint: fingerprint,
mode: "auto-with-overrides",
basePreset: "technical",
slots,
diagnostics: []
};
}
function createOptions(exportConfig: ExportConfig) {
return {
exportConfig,
@@ -137,7 +156,8 @@ function createOptions(exportConfig: ExportConfig) {
metadata: {
title: "年度 <报告>",
author: "测试人"
}
},
themeTokens: themeTokens()
};
}
@@ -159,6 +179,10 @@ describe("动态 reference.docx", () => {
author: "测试人"
}
});
const changedTokens = createDynamicReferenceDocx(baseline, {
...createOptions(defaultExportConfig),
themeTokens: themeTokens("d".repeat(64))
});
const entries = unzipSync(first.content);
const documentXml = decoder.decode(entries["word/document.xml"]);
const stylesXml = decoder.decode(entries["word/styles.xml"]);
@@ -169,6 +193,7 @@ describe("动态 reference.docx", () => {
);
expect(first.cacheKey).toBe(second.cacheKey);
expect(first.cacheKey).not.toBe(changedMetadata.cacheKey);
expect(first.cacheKey).not.toBe(changedTokens.cacheKey);
expect(documentXml).toContain('w:w="11906"');
expect(documentXml).toContain('w:h="16838"');
expect(documentXml).toContain('w:top="907"');
@@ -186,6 +211,106 @@ describe("动态 reference.docx", () => {
});
});
it("将通用主题令牌写入标准样式、结构样式和字体部件", () => {
const result = createDynamicReferenceDocx(
createBaselineReference(),
{
...createOptions(defaultExportConfig),
themeTokens: themeTokens("e".repeat(64), [
{
slot: "paragraph",
source: "computed-css",
confidence: "exact",
style: {
fontCandidates: [
"Source Han Serif SC",
"Times New Roman"
],
fontSizePt: 14,
color: "#112233",
lineSpacing: 1.8,
firstLineIndentPt: 28
}
},
{
slot: "heading-1",
source: "computed-css",
confidence: "exact",
style: {
fontCandidates: ["SimHei", "Arial"],
fontSizePt: 22,
bold: true,
color: "#aa0000",
alignment: "center",
keepWithNext: true
}
},
{
slot: "table",
source: "computed-css",
confidence: "exact",
style: {
fontCandidates: [],
widthPercent: 100,
paddingPt: {
top: 3,
right: 4,
bottom: 3,
left: 4
},
borders: {
top: {
widthPt: 1,
color: "#445566",
style: "single"
}
}
}
},
{
slot: "official-title",
source: "computed-css",
confidence: "exact",
style: {
fontCandidates: ["SimSun"],
fontSizePt: 22,
color: "#ff0000",
alignment: "center",
keepWithNext: true
}
}
])
}
);
const entries = unzipSync(result.content);
const stylesXml = decoder.decode(entries["word/styles.xml"]);
const fontTableXml = decoder.decode(
entries["word/fontTable.xml"]
);
const themeXml = decoder.decode(
entries["word/theme/theme1.xml"]
);
expect(stylesXml).toContain('w:styleId="MdOfficialTitle"');
expect(stylesXml).toContain('w:eastAsia="Source Han Serif SC"');
expect(stylesXml).toContain('w:ascii="Times New Roman"');
expect(stylesXml).toContain('w:val="112233"');
expect(stylesXml).toContain('w:styleId="Heading1"');
expect(stylesXml).toContain('w:val="AA0000"');
expect(stylesXml).toContain('w:w="5000" w:type="pct"');
expect(stylesXml).toContain('w:color="445566"');
expect(fontTableXml).toContain(
'w:name="Source Han Serif SC"'
);
expect(fontTableXml).toContain('w:name="SimHei"');
expect(themeXml).toContain(
'<a:latin typeface="Arial"'
);
expect(themeXml).toContain(
'<a:ea typeface="SimHei"'
);
});
it("生成横向、自定义页边距和奇偶首页页眉页脚", () => {
const exportConfig: ExportConfig = {
...defaultExportConfig,