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
@@ -13,6 +13,7 @@ import {
type PreparedDocxMedia,
type ThemeManifest
} from "@md-to-pdf/core";
import type { DocxThemeTokenSet } from "@md-to-pdf/docx-theme-engine";
import {
PandocDocxConversionError,
PandocDocxConverter,
@@ -49,6 +50,16 @@ const emptyMedia: PreparedDocxMedia = {
totalBytes: 0
};
const themeTokens: DocxThemeTokenSet = {
schemaVersion: 1,
themeId: "test-theme",
themeFingerprint: "c".repeat(64),
mode: "auto-with-overrides",
basePreset: "technical",
slots: [],
diagnostics: []
};
function input() {
return {
markdown: "# 测试",
@@ -56,6 +67,7 @@ function input() {
language: "zh-CN",
exportConfig: defaultExportConfig,
theme: theme(),
themeTokens,
metadata: {
title: "测试",
author: "测试人",
@@ -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,
@@ -0,0 +1,76 @@
import { describe, expect, it } from "vitest";
import type { DocxThemeTokenSet } from "@md-to-pdf/docx-theme-engine";
import {
DOCX_SLOT_WORD_STYLE_BINDINGS,
collectDocxTokenFonts,
createDocxTokenSlotMap,
resolveTokenFonts
} from "../src/index.js";
const tokens: DocxThemeTokenSet = {
schemaVersion: 1,
themeId: "test-theme",
themeFingerprint: "a".repeat(64),
mode: "auto",
basePreset: "general",
slots: [
{
slot: "paragraph",
source: "computed-css",
confidence: "exact",
style: {
fontCandidates: [
"Source Han Serif SC",
"Times New Roman",
"serif"
]
}
}
],
diagnostics: []
};
describe("DOCX 令牌样式映射", () => {
it("以稳定样式 ID 覆盖标准 Markdown 和结构槽位", () => {
expect(
DOCX_SLOT_WORD_STYLE_BINDINGS["heading-1"]
).toEqual([
{
styleId: "Heading1",
type: "paragraph",
basedOn: "Normal"
},
{
styleId: "Heading1Char",
type: "character",
basedOn: "DefaultParagraphFont"
}
]);
expect(
DOCX_SLOT_WORD_STYLE_BINDINGS["official-title"]?.[0]
?.styleId
).toBe("MdOfficialTitle");
expect(
DOCX_SLOT_WORD_STYLE_BINDINGS["tender-representative"]?.[0]
?.styleId
).toBe("MdTenderRepresentative");
});
it("按文字系统选择字体并过滤 CSS 通用族名", () => {
const slot = createDocxTokenSlotMap(tokens).get("paragraph");
expect(
resolveTokenFonts(slot?.style, {
latin: "Arial",
eastAsia: "SimSun"
})
).toEqual({
latin: "Times New Roman",
eastAsia: "Source Han Serif SC",
complexScript: "Times New Roman"
});
expect([...collectDocxTokenFonts(tokens)]).toEqual([
"Source Han Serif SC",
"Times New Roman"
]);
});
});