feat: 建立 DOCX 主题映射引擎

This commit is contained in:
SkyJourney
2026-07-30 20:28:47 +08:00
parent e7c688df6e
commit 6fed666f44
15 changed files with 851 additions and 25 deletions
+29 -8
View File
@@ -27,6 +27,16 @@ export type DocxStylePreset = z.infer<
typeof docxStylePresetSchema
>;
export const docxThemeStyleModeSchema = z.enum([
"auto",
"auto-with-overrides",
"explicit"
]);
export type DocxThemeStyleMode = z.infer<
typeof docxThemeStyleModeSchema
>;
export const docxFontFamilySchema = z.object({
latin: fontNameSchema,
eastAsia: fontNameSchema,
@@ -37,7 +47,7 @@ export type DocxFontFamily = z.infer<
typeof docxFontFamilySchema
>;
const docxBodyStyleSchema = z.object({
export const docxBodyStyleSchema = z.object({
fonts: docxFontFamilySchema.optional(),
sizePt: pointSizeSchema.optional(),
color: colorSchema.optional(),
@@ -47,7 +57,7 @@ const docxBodyStyleSchema = z.object({
spacingAfterPt: spacingSchema.optional()
});
const docxHeadingStyleSchema = z.object({
export const docxHeadingStyleSchema = z.object({
fonts: docxFontFamilySchema.optional(),
sizesPt: z
.tuple([
@@ -65,7 +75,7 @@ const docxHeadingStyleSchema = z.object({
spacingAfterPt: spacingSchema.optional()
});
const docxCodeStyleSchema = z.object({
export const docxCodeStyleSchema = z.object({
fonts: docxFontFamilySchema.optional(),
sizePt: pointSizeSchema.optional(),
color: colorSchema.optional(),
@@ -74,7 +84,7 @@ const docxCodeStyleSchema = z.object({
lineSpacing: z.number().min(1).max(3).optional()
});
const docxBlockStyleSchema = z.object({
export const docxBlockStyleSchema = z.object({
color: colorSchema.optional(),
backgroundColor: colorSchema.optional(),
borderColor: colorSchema.optional(),
@@ -82,7 +92,7 @@ const docxBlockStyleSchema = z.object({
italic: z.boolean().optional()
});
const docxTableStyleSchema = z.object({
export const docxTableStyleSchema = z.object({
fonts: docxFontFamilySchema.optional(),
sizePt: pointSizeSchema.optional(),
color: colorSchema.optional(),
@@ -92,7 +102,7 @@ const docxTableStyleSchema = z.object({
cellMarginMm: z.number().min(0).max(20).optional()
});
const docxCaptionStyleSchema = z.object({
export const docxCaptionStyleSchema = z.object({
fonts: docxFontFamilySchema.optional(),
sizePt: pointSizeSchema.optional(),
color: colorSchema.optional(),
@@ -100,8 +110,7 @@ const docxCaptionStyleSchema = z.object({
alignment: z.enum(["left", "center", "right"]).optional()
});
export const docxThemeStyleSchema = z.object({
preset: docxStylePresetSchema,
export const docxThemeStyleOverridesSchema = z.object({
body: docxBodyStyleSchema.optional(),
headings: docxHeadingStyleSchema.optional(),
code: docxCodeStyleSchema.optional(),
@@ -116,6 +125,18 @@ export const docxThemeStyleSchema = z.object({
.optional()
});
export type DocxThemeStyleOverrides = z.infer<
typeof docxThemeStyleOverridesSchema
>;
export const docxThemeStyleSchema =
docxThemeStyleOverridesSchema.extend({
mode: docxThemeStyleModeSchema.optional(),
preset: docxStylePresetSchema.optional(),
basePreset: docxStylePresetSchema.optional(),
overrides: docxThemeStyleOverridesSchema.optional()
});
export type DocxThemeStyle = z.infer<
typeof docxThemeStyleSchema
>;
+22
View File
@@ -209,4 +209,26 @@ describe("DOCX 主题样式协议", () => {
})
).toThrow();
});
it("接受自动映射模式和兼容预设", () => {
expect(
docxThemeStyleSchema.parse({
mode: "auto-with-overrides",
basePreset: "technical",
overrides: {
table: {
headerBackgroundColor: "#f6f8fa"
}
}
})
).toMatchObject({
mode: "auto-with-overrides",
basePreset: "technical",
overrides: {
table: {
headerBackgroundColor: "#f6f8fa"
}
}
});
});
});
+31 -11
View File
@@ -234,25 +234,45 @@ export function resolveDocxThemeStyle(
preset: inferDocxStylePreset(manifest)
}
);
const preset = presetOverrides[declared.preset];
const presetName =
declared.basePreset ??
declared.preset ??
inferDocxStylePreset(manifest);
const preset = presetOverrides[presetName];
const overrides = declared.overrides;
const base = {
...baseStyle,
...preset
};
return {
preset: declared.preset,
body: mergeSection(base.body, declared.body),
headings: mergeSection(base.headings, declared.headings),
code: mergeSection(base.code, declared.code),
preset: presetName,
body: mergeSection(
mergeSection(base.body, declared.body),
overrides?.body
),
headings: mergeSection(
mergeSection(base.headings, declared.headings),
overrides?.headings
),
code: mergeSection(
mergeSection(base.code, declared.code),
overrides?.code
),
blockQuote: mergeSection(
base.blockQuote,
declared.blockQuote
mergeSection(base.blockQuote, declared.blockQuote),
overrides?.blockQuote
),
table: mergeSection(
mergeSection(base.table, declared.table),
overrides?.table
),
caption: mergeSection(
mergeSection(base.caption, declared.caption),
overrides?.caption
),
table: mergeSection(base.table, declared.table),
caption: mergeSection(base.caption, declared.caption),
hyperlink: mergeSection(
base.hyperlink,
declared.hyperlink
mergeSection(base.hyperlink, declared.hyperlink),
overrides?.hyperlink
)
};
}
+25
View File
@@ -0,0 +1,25 @@
# @md-to-pdf/docx-theme-engine
DOCX 主题映射引擎负责在浏览器主题 CSS 与 Word 样式之间建立稳定、可诊断
的中间协议。
当前阶段包含:
- 标准 Markdown 与结构化文档语义槽位;
- Chromium/Electron 计算样式快照协议;
- Word 目标样式令牌;
- 自动映射、显式覆盖和预设降级配置;
- 样式来源、映射置信度和诊断协议;
- 旧版 `docxStyle.preset` 兼容归一化。
本包不负责:
- 启动 Playwright 或 Electron
- 读取主题目录或网络资源;
- 执行 Pandoc
- 直接修改 OOXML
- 按主题 ID 维护专属转换分支。
后续 Chromium 与 Electron 适配器只负责采集标准样式探针的
`getComputedStyle()` 结果。本包负责将快照归一化为 DOCX 样式令牌,
`@md-to-pdf/docx-engine` 再消费令牌生成 `reference.docx` 和最终 OOXML。
+30
View File
@@ -0,0 +1,30 @@
{
"name": "@md-to-pdf/docx-theme-engine",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
"build": "tsc -p tsconfig.json",
"test": "vitest run",
"typecheck": "tsc -p tsconfig.json --noEmit --pretty false"
},
"dependencies": {
"@md-to-pdf/core": "0.1.0",
"zod": "^4.1.13"
},
"devDependencies": {
"vitest": "^4.1.10"
}
}
@@ -0,0 +1,104 @@
import {
docxThemeStyleOverridesSchema,
type DocxStylePreset,
type DocxThemeStyleMode,
type DocxThemeStyleOverrides,
type ThemeManifest
} from "@md-to-pdf/core";
export interface NormalizedDocxThemeMappingConfig {
mode: DocxThemeStyleMode;
basePreset: DocxStylePreset;
overrides: DocxThemeStyleOverrides;
legacyPreset: boolean;
}
function inferBasePreset(
manifest: Pick<ThemeManifest, "category">
): DocxStylePreset {
if (manifest.category === "red-letter") {
return "official";
}
if (manifest.category === "formal") {
return "formal";
}
if (manifest.category === "tender") {
return "tender";
}
return "general";
}
function mergeSection<T extends object>(
legacy: T | undefined,
current: T | undefined
): T | undefined {
if (!legacy && !current) {
return undefined;
}
return {
...legacy,
...current
} as T;
}
export function normalizeDocxThemeMappingConfig(
manifest: ThemeManifest
): NormalizedDocxThemeMappingConfig {
const style = manifest.docxStyle;
const legacyPreset =
style?.preset !== undefined &&
style.mode === undefined &&
style.basePreset === undefined;
const mode =
style?.mode ??
(legacyPreset ? "explicit" : "auto");
const basePreset =
style?.basePreset ??
style?.preset ??
inferBasePreset(manifest);
const legacyOverrides = docxThemeStyleOverridesSchema.parse({
body: style?.body,
headings: style?.headings,
code: style?.code,
blockQuote: style?.blockQuote,
table: style?.table,
caption: style?.caption,
hyperlink: style?.hyperlink
});
const overrides = docxThemeStyleOverridesSchema.parse({
body: mergeSection(
legacyOverrides.body,
style?.overrides?.body
),
headings: mergeSection(
legacyOverrides.headings,
style?.overrides?.headings
),
code: mergeSection(
legacyOverrides.code,
style?.overrides?.code
),
blockQuote: mergeSection(
legacyOverrides.blockQuote,
style?.overrides?.blockQuote
),
table: mergeSection(
legacyOverrides.table,
style?.overrides?.table
),
caption: mergeSection(
legacyOverrides.caption,
style?.overrides?.caption
),
hyperlink: mergeSection(
legacyOverrides.hyperlink,
style?.overrides?.hyperlink
)
});
return {
mode,
basePreset,
overrides,
legacyPreset
};
}
+4
View File
@@ -0,0 +1,4 @@
export * from "./configuration.js";
export * from "./slots.js";
export * from "./snapshot.js";
export * from "./tokens.js";
+149
View File
@@ -0,0 +1,149 @@
import { z } from "zod";
export const DOCX_STYLE_SLOT_NAMES = [
"document",
"document-title",
"document-author",
"paragraph",
"strong",
"emphasis",
"strikethrough",
"inline-code",
"heading-1",
"heading-2",
"heading-3",
"heading-4",
"heading-5",
"heading-6",
"unordered-list",
"ordered-list",
"list-item",
"block-quote",
"code-block",
"table",
"table-header",
"table-cell",
"hyperlink",
"figure",
"image",
"caption",
"footnotes",
"official-masthead",
"official-classification",
"official-issuer",
"official-issue-row",
"official-number",
"official-signatory",
"official-signature",
"official-edition",
"briefing-masthead",
"briefing-meta",
"briefing-contact",
"cover",
"cover-copy-mark",
"cover-project-name",
"cover-project-number",
"cover-title",
"cover-volume",
"cover-owner",
"cover-prepared-by",
"cover-bidder",
"cover-representative",
"cover-version",
"cover-date"
] as const;
export const docxStyleSlotNameSchema = z.enum(
DOCX_STYLE_SLOT_NAMES
);
export type DocxStyleSlotName = z.infer<
typeof docxStyleSlotNameSchema
>;
export const docxStyleSlotKindSchema = z.enum([
"document",
"paragraph",
"inline",
"list",
"table",
"media",
"structure"
]);
export type DocxStyleSlotKind = z.infer<
typeof docxStyleSlotKindSchema
>;
export interface DocxStyleSlotDefinition {
name: DocxStyleSlotName;
kind: DocxStyleSlotKind;
selector: string;
}
function slot(
name: DocxStyleSlotName,
kind: DocxStyleSlotKind
): DocxStyleSlotDefinition {
return {
name,
kind,
selector: `[data-docx-slot="${name}"]`
};
}
const kinds: Record<DocxStyleSlotName, DocxStyleSlotKind> = {
document: "document",
"document-title": "paragraph",
"document-author": "paragraph",
paragraph: "paragraph",
strong: "inline",
emphasis: "inline",
strikethrough: "inline",
"inline-code": "inline",
"heading-1": "paragraph",
"heading-2": "paragraph",
"heading-3": "paragraph",
"heading-4": "paragraph",
"heading-5": "paragraph",
"heading-6": "paragraph",
"unordered-list": "list",
"ordered-list": "list",
"list-item": "list",
"block-quote": "paragraph",
"code-block": "paragraph",
table: "table",
"table-header": "table",
"table-cell": "table",
hyperlink: "inline",
figure: "media",
image: "media",
caption: "paragraph",
footnotes: "paragraph",
"official-masthead": "structure",
"official-classification": "structure",
"official-issuer": "structure",
"official-issue-row": "structure",
"official-number": "structure",
"official-signatory": "structure",
"official-signature": "structure",
"official-edition": "structure",
"briefing-masthead": "structure",
"briefing-meta": "structure",
"briefing-contact": "structure",
cover: "structure",
"cover-copy-mark": "structure",
"cover-project-name": "structure",
"cover-project-number": "structure",
"cover-title": "structure",
"cover-volume": "structure",
"cover-owner": "structure",
"cover-prepared-by": "structure",
"cover-bidder": "structure",
"cover-representative": "structure",
"cover-version": "structure",
"cover-date": "structure"
};
export const DOCX_STYLE_SLOTS: readonly DocxStyleSlotDefinition[] =
DOCX_STYLE_SLOT_NAMES.map((name) => slot(name, kinds[name]));
@@ -0,0 +1,90 @@
import { z } from "zod";
import { docxStyleSlotNameSchema } from "./slots.js";
const cssValueSchema = z.string().trim().min(1).max(500);
export const docxComputedStyleSchema = z.object({
fontFamily: cssValueSchema,
fontSize: cssValueSchema,
fontWeight: cssValueSchema,
fontStyle: cssValueSchema,
color: cssValueSchema,
backgroundColor: cssValueSchema,
lineHeight: cssValueSchema,
letterSpacing: cssValueSchema,
textAlign: cssValueSchema,
textIndent: cssValueSchema,
textDecorationLine: cssValueSchema,
marginTop: cssValueSchema,
marginRight: cssValueSchema,
marginBottom: cssValueSchema,
marginLeft: cssValueSchema,
paddingTop: cssValueSchema,
paddingRight: cssValueSchema,
paddingBottom: cssValueSchema,
paddingLeft: cssValueSchema,
borderTop: cssValueSchema,
borderRight: cssValueSchema,
borderBottom: cssValueSchema,
borderLeft: cssValueSchema,
width: cssValueSchema,
maxWidth: cssValueSchema,
breakBefore: cssValueSchema,
breakAfter: cssValueSchema,
breakInside: cssValueSchema,
display: cssValueSchema
});
export type DocxComputedStyle = z.infer<
typeof docxComputedStyleSchema
>;
export const docxStyleSlotSnapshotSchema = z.object({
slot: docxStyleSlotNameSchema,
matched: z.boolean(),
computed: docxComputedStyleSchema.optional()
});
export type DocxStyleSlotSnapshot = z.infer<
typeof docxStyleSlotSnapshotSchema
>;
export const docxThemeStyleSnapshotSchema = z
.object({
schemaVersion: z.literal(1),
themeId: z
.string()
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/u),
themeFingerprint: z.string().regex(/^[a-f0-9]{64}$/u),
viewport: z.object({
widthPx: z.number().int().positive().max(10000),
heightPx: z.number().int().positive().max(10000),
deviceScaleFactor: z.number().positive().max(10)
}),
rootFontSizePx: z.number().positive().max(200),
slots: z.array(docxStyleSlotSnapshotSchema).min(1)
})
.superRefine((snapshot, context) => {
const names = new Set<string>();
for (const entry of snapshot.slots) {
if (names.has(entry.slot)) {
context.addIssue({
code: "custom",
message: `样式快照包含重复槽位:${entry.slot}`,
path: ["slots"]
});
}
names.add(entry.slot);
if (entry.matched !== Boolean(entry.computed)) {
context.addIssue({
code: "custom",
message: `槽位 ${entry.slot} 的匹配状态与计算样式不一致`,
path: ["slots"]
});
}
}
});
export type DocxThemeStyleSnapshot = z.infer<
typeof docxThemeStyleSnapshotSchema
>;
+158
View File
@@ -0,0 +1,158 @@
import {
docxStylePresetSchema,
docxThemeStyleModeSchema
} from "@md-to-pdf/core";
import { z } from "zod";
import { docxStyleSlotNameSchema } from "./slots.js";
const colorSchema = z
.string()
.regex(/^#[0-9a-f]{6}$/iu, "使");
export const docxStyleValueSourceSchema = z.enum([
"computed-css",
"manifest-override",
"preset-fallback",
"engine-default"
]);
export type DocxStyleValueSource = z.infer<
typeof docxStyleValueSourceSchema
>;
export const docxStyleConfidenceSchema = z.enum([
"exact",
"approximate",
"fallback"
]);
export type DocxStyleConfidence = z.infer<
typeof docxStyleConfidenceSchema
>;
export const docxThemeDiagnosticSeveritySchema = z.enum([
"info",
"warning",
"error"
]);
export const docxThemeDiagnosticCodeSchema = z.enum([
"slot-not-found",
"css-value-invalid",
"css-property-unsupported",
"font-fallback-required",
"layout-approximated",
"preset-fallback-used",
"manifest-override-applied"
]);
export const docxThemeDiagnosticSchema = z.object({
severity: docxThemeDiagnosticSeveritySchema,
code: docxThemeDiagnosticCodeSchema,
message: z.string().min(1).max(500),
slot: docxStyleSlotNameSchema.optional(),
property: z.string().min(1).max(100).optional()
});
export type DocxThemeDiagnostic = z.infer<
typeof docxThemeDiagnosticSchema
>;
const docxBorderTokenSchema = z.object({
widthPt: z.number().min(0).max(20),
color: colorSchema,
style: z.enum([
"none",
"single",
"double",
"dotted",
"dashed"
])
});
export const docxSlotStyleTokenSchema = z.object({
fontCandidates: z.array(z.string().trim().min(1).max(100)).max(20),
fontSizePt: z.number().min(1).max(200).optional(),
bold: z.boolean().optional(),
italic: z.boolean().optional(),
underline: z.boolean().optional(),
strikethrough: z.boolean().optional(),
color: colorSchema.optional(),
backgroundColor: colorSchema.optional(),
lineSpacing: z.number().min(0.5).max(10).optional(),
letterSpacingPt: z.number().min(-20).max(100).optional(),
alignment: z
.enum(["left", "center", "right", "justify"])
.optional(),
firstLineIndentPt: z.number().min(-1000).max(1000).optional(),
leftIndentPt: z.number().min(-1000).max(1000).optional(),
rightIndentPt: z.number().min(-1000).max(1000).optional(),
spacingBeforePt: z.number().min(0).max(2000).optional(),
spacingAfterPt: z.number().min(0).max(2000).optional(),
paddingPt: z
.object({
top: z.number().min(0).max(1000),
right: z.number().min(0).max(1000),
bottom: z.number().min(0).max(1000),
left: z.number().min(0).max(1000)
})
.optional(),
borders: z
.object({
top: docxBorderTokenSchema.optional(),
right: docxBorderTokenSchema.optional(),
bottom: docxBorderTokenSchema.optional(),
left: docxBorderTokenSchema.optional()
})
.optional(),
widthPercent: z.number().min(0).max(100).optional(),
pageBreakBefore: z.boolean().optional(),
pageBreakAfter: z.boolean().optional(),
keepLines: z.boolean().optional(),
keepWithNext: z.boolean().optional()
});
export type DocxSlotStyleToken = z.infer<
typeof docxSlotStyleTokenSchema
>;
export const docxResolvedStyleSlotSchema = z.object({
slot: docxStyleSlotNameSchema,
source: docxStyleValueSourceSchema,
confidence: docxStyleConfidenceSchema,
style: docxSlotStyleTokenSchema
});
export type DocxResolvedStyleSlot = z.infer<
typeof docxResolvedStyleSlotSchema
>;
export const docxThemeTokenSetSchema = z
.object({
schemaVersion: z.literal(1),
themeId: z
.string()
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/u),
themeFingerprint: z.string().regex(/^[a-f0-9]{64}$/u),
mode: docxThemeStyleModeSchema,
basePreset: docxStylePresetSchema,
slots: z.array(docxResolvedStyleSlotSchema),
diagnostics: z.array(docxThemeDiagnosticSchema)
})
.superRefine((tokens, context) => {
const names = new Set<string>();
for (const entry of tokens.slots) {
if (names.has(entry.slot)) {
context.addIssue({
code: "custom",
message: `DOCX 样式令牌包含重复槽位:${entry.slot}`,
path: ["slots"]
});
}
names.add(entry.slot);
}
});
export type DocxThemeTokenSet = z.infer<
typeof docxThemeTokenSetSchema
>;
@@ -0,0 +1,162 @@
import { themeManifestSchema } from "@md-to-pdf/core";
import { describe, expect, it } from "vitest";
import {
DOCX_STYLE_SLOTS,
docxThemeStyleSnapshotSchema,
docxThemeTokenSetSchema,
normalizeDocxThemeMappingConfig
} from "../src/index.js";
function createManifest(docxStyle?: unknown) {
return themeManifestSchema.parse({
manifestVersion: 1,
id: "external-clean",
name: "External Clean",
version: "1.0.0",
description: "外部主题",
author: "Tester",
license: "MIT",
entry: "theme.css",
domPreset: "generic",
defaultFontSize: "16px",
supportedFeatures: ["table"],
category: "general",
compatibleProfiles: [],
...(docxStyle ? { docxStyle } : {}),
bundled: false
});
}
describe("DOCX 主题映射配置", () => {
it("没有 DOCX 声明的外部主题默认使用自动映射", () => {
expect(
normalizeDocxThemeMappingConfig(createManifest())
).toEqual({
mode: "auto",
basePreset: "general",
overrides: {},
legacyPreset: false
});
});
it("兼容现有 preset 与扁平覆盖", () => {
const result = normalizeDocxThemeMappingConfig(
createManifest({
preset: "technical",
body: { sizePt: 11 }
})
);
expect(result).toMatchObject({
mode: "explicit",
basePreset: "technical",
overrides: { body: { sizePt: 11 } },
legacyPreset: true
});
});
it("新式 overrides 优先于旧式扁平覆盖", () => {
const result = normalizeDocxThemeMappingConfig(
createManifest({
mode: "auto-with-overrides",
basePreset: "formal",
body: { sizePt: 11, color: "#111111" },
overrides: { body: { sizePt: 12 } }
})
);
expect(result.overrides.body?.sizePt).toBe(12);
expect(result.overrides.body?.color).toBe("#111111");
});
});
describe("DOCX 主题引擎契约", () => {
it("样式槽位名称与选择器保持唯一", () => {
expect(new Set(DOCX_STYLE_SLOTS.map((slot) => slot.name)).size)
.toBe(DOCX_STYLE_SLOTS.length);
expect(
new Set(DOCX_STYLE_SLOTS.map((slot) => slot.selector)).size
).toBe(DOCX_STYLE_SLOTS.length);
});
it("拒绝重复或状态不一致的样式快照", () => {
const computed = {
fontFamily: "Arial",
fontSize: "16px",
fontWeight: "400",
fontStyle: "normal",
color: "rgb(0, 0, 0)",
backgroundColor: "rgba(0, 0, 0, 0)",
lineHeight: "24px",
letterSpacing: "normal",
textAlign: "start",
textIndent: "0px",
textDecorationLine: "none",
marginTop: "0px",
marginRight: "0px",
marginBottom: "0px",
marginLeft: "0px",
paddingTop: "0px",
paddingRight: "0px",
paddingBottom: "0px",
paddingLeft: "0px",
borderTop: "0px none rgb(0, 0, 0)",
borderRight: "0px none rgb(0, 0, 0)",
borderBottom: "0px none rgb(0, 0, 0)",
borderLeft: "0px none rgb(0, 0, 0)",
width: "100px",
maxWidth: "none",
breakBefore: "auto",
breakAfter: "auto",
breakInside: "auto",
display: "block"
};
const result = docxThemeStyleSnapshotSchema.safeParse({
schemaVersion: 1,
themeId: "external-clean",
themeFingerprint: "a".repeat(64),
viewport: {
widthPx: 794,
heightPx: 1123,
deviceScaleFactor: 1
},
rootFontSizePx: 16,
slots: [
{ slot: "paragraph", matched: true, computed },
{ slot: "paragraph", matched: false }
]
});
expect(result.success).toBe(false);
});
it("接受带来源、置信度和诊断的样式令牌", () => {
expect(
docxThemeTokenSetSchema.parse({
schemaVersion: 1,
themeId: "external-clean",
themeFingerprint: "b".repeat(64),
mode: "auto",
basePreset: "general",
slots: [
{
slot: "paragraph",
source: "computed-css",
confidence: "exact",
style: {
fontCandidates: ["Arial", "sans-serif"],
fontSizePt: 12,
color: "#111111"
}
}
],
diagnostics: [
{
severity: "warning",
code: "font-fallback-required",
message: "首选字体不可用",
slot: "paragraph",
property: "font-family"
}
]
}).slots
).toHaveLength(1);
});
});
+16
View File
@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "src",
"outDir": "dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": [
"src"
]
}