feat: 将主题令牌接入 DOCX 动态模板
This commit is contained in:
@@ -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"
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user