feat: 建立 DOCX 主题映射引擎
This commit is contained in:
+11
-2
@@ -152,6 +152,14 @@ DOCX 样式令牌,支持自动映射、清单覆盖和预设降级;Front Mat
|
|||||||
主题 ID 分支,普通外部主题即使没有 `docxStyle` 也应获得可诊断的基础
|
主题 ID 分支,普通外部主题即使没有 `docxStyle` 也应获得可诊断的基础
|
||||||
DOCX 映射。
|
DOCX 映射。
|
||||||
|
|
||||||
|
`v0.6.0` 阶段 11 的协议骨架已建立为独立
|
||||||
|
`packages/docx-theme-engine`:定义 50 个稳定语义样式槽位、浏览器计算
|
||||||
|
样式快照、Word 目标样式令牌、属性来源、映射置信度和诊断协议。
|
||||||
|
`docxStyle` 支持 `auto`、`auto-with-overrides`、`explicit` 三种模式,
|
||||||
|
普通外部主题缺少 DOCX 声明时默认进入自动映射;现有主题的 `preset` 和
|
||||||
|
扁平样式覆盖保持兼容。当前只建立公共契约与 workspace 构建边界,尚未
|
||||||
|
实现样式探针页面、Chromium/Electron 采集器或接入 Pandoc 转换链路。
|
||||||
|
|
||||||
## 2. 已完成
|
## 2. 已完成
|
||||||
|
|
||||||
### 2.1 项目骨架
|
### 2.1 项目骨架
|
||||||
@@ -1076,8 +1084,9 @@ ECharts 第二阶段浏览器与 PDF 验证结果:
|
|||||||
Desktop 保存验收;
|
Desktop 保存验收;
|
||||||
- 阶段 10:已完成 Word 原生页眉页脚和第一轮全主题视觉审计;完整主题
|
- 阶段 10:已完成 Word 原生页眉页脚和第一轮全主题视觉审计;完整主题
|
||||||
映射门禁未通过;
|
映射门禁未通过;
|
||||||
- 阶段 11:建立独立 `packages/docx-theme-engine`、标准样式探针和
|
- 阶段 11:已建立独立 `packages/docx-theme-engine`、标准语义槽位及
|
||||||
CSS 计算样式到 DOCX 令牌的通用映射;
|
快照、令牌、来源、置信度和诊断协议;下一步实现样式探针与
|
||||||
|
Chromium/Electron 计算样式采集;
|
||||||
- 阶段 12:建立统一语义文档模型,完成 Front Matter、封面、分节、
|
- 阶段 12:建立统一语义文档模型,完成 Front Matter、封面、分节、
|
||||||
表格宽度和分页控制;
|
表格宽度和分页控制;
|
||||||
- 阶段 13:完成 Word/WPS 双向互存、外部主题兼容、体积和正式发布验收。
|
- 阶段 13:完成 Word/WPS 双向互存、外部主题兼容、体积和正式发布验收。
|
||||||
|
|||||||
Generated
+15
@@ -2189,6 +2189,10 @@
|
|||||||
"resolved": "packages/docx-engine",
|
"resolved": "packages/docx-engine",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@md-to-pdf/docx-theme-engine": {
|
||||||
|
"resolved": "packages/docx-theme-engine",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@md-to-pdf/markdown-echarts": {
|
"node_modules/@md-to-pdf/markdown-echarts": {
|
||||||
"resolved": "packages/markdown-echarts",
|
"resolved": "packages/markdown-echarts",
|
||||||
"link": true
|
"link": true
|
||||||
@@ -9936,6 +9940,17 @@
|
|||||||
"vitest": "^4.1.10"
|
"vitest": "^4.1.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/docx-theme-engine": {
|
||||||
|
"name": "@md-to-pdf/docx-theme-engine",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@md-to-pdf/core": "0.1.0",
|
||||||
|
"zod": "^4.1.13"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitest": "^4.1.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/markdown-echarts": {
|
"packages/markdown-echarts": {
|
||||||
"name": "@md-to-pdf/markdown-echarts",
|
"name": "@md-to-pdf/markdown-echarts",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
|||||||
+5
-4
@@ -10,10 +10,11 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build:web-runtime && npm run build -w @md-to-pdf/server && npm run build -w @md-to-pdf/desktop",
|
"build": "npm run build:web-runtime && npm run build -w @md-to-pdf/server && npm run build -w @md-to-pdf/desktop",
|
||||||
"build:web-runtime": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/web",
|
"build:web-runtime": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/web",
|
||||||
"dev": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/preview-engine && concurrently -k -n markdown-echarts,core,docx-engine,renderer,application,preview-engine,server,web \"npm:dev:markdown-echarts\" \"npm:dev:core\" \"npm:dev:docx-engine\" \"npm:dev:renderer\" \"npm:dev:application\" \"npm:dev:preview-engine\" \"npm:dev:server\" \"npm:dev:web\"",
|
"dev": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/preview-engine && concurrently -k -n markdown-echarts,core,docx-theme-engine,docx-engine,renderer,application,preview-engine,server,web \"npm:dev:markdown-echarts\" \"npm:dev:core\" \"npm:dev:docx-theme-engine\" \"npm:dev:docx-engine\" \"npm:dev:renderer\" \"npm:dev:application\" \"npm:dev:preview-engine\" \"npm:dev:server\" \"npm:dev:web\"",
|
||||||
"dev:markdown-echarts": "npm run dev -w @md-to-pdf/markdown-echarts",
|
"dev:markdown-echarts": "npm run dev -w @md-to-pdf/markdown-echarts",
|
||||||
"dev:core": "npm run dev -w @md-to-pdf/core",
|
"dev:core": "npm run dev -w @md-to-pdf/core",
|
||||||
|
"dev:docx-theme-engine": "npm run dev -w @md-to-pdf/docx-theme-engine",
|
||||||
"dev:docx-engine": "npm run dev -w @md-to-pdf/docx-engine",
|
"dev:docx-engine": "npm run dev -w @md-to-pdf/docx-engine",
|
||||||
"dev:renderer": "npm run dev -w @md-to-pdf/renderer",
|
"dev:renderer": "npm run dev -w @md-to-pdf/renderer",
|
||||||
"dev:application": "npm run dev -w @md-to-pdf/application",
|
"dev:application": "npm run dev -w @md-to-pdf/application",
|
||||||
@@ -30,8 +31,8 @@
|
|||||||
"verify:docx-themes": "npm run build:web-runtime && npm run verify:themes -w @md-to-pdf/docx-engine",
|
"verify:docx-themes": "npm run build:web-runtime && npm run verify:themes -w @md-to-pdf/docx-engine",
|
||||||
"verify:docx-acceptance": "npm run build:web-runtime && npm run build -w @md-to-pdf/server && npm run build -w @md-to-pdf/desktop && npm run verify:pandoc -w @md-to-pdf/docx-engine && npm run verify:conversion -w @md-to-pdf/docx-engine && npm run verify:acceptance -w @md-to-pdf/docx-engine && npm run verify:docx-http -w @md-to-pdf/server && npm run test -w @md-to-pdf/desktop -- desktop-docx-save.test.ts",
|
"verify:docx-acceptance": "npm run build:web-runtime && npm run build -w @md-to-pdf/server && npm run build -w @md-to-pdf/desktop && npm run verify:pandoc -w @md-to-pdf/docx-engine && npm run verify:conversion -w @md-to-pdf/docx-engine && npm run verify:acceptance -w @md-to-pdf/docx-engine && npm run verify:docx-http -w @md-to-pdf/server && npm run test -w @md-to-pdf/desktop -- desktop-docx-save.test.ts",
|
||||||
"verify:docx-http": "npm run build:web-runtime && npm run build -w @md-to-pdf/server && npm run verify:docx-http -w @md-to-pdf/server",
|
"verify:docx-http": "npm run build:web-runtime && npm run build -w @md-to-pdf/server && npm run verify:docx-http -w @md-to-pdf/server",
|
||||||
"test": "npm run test -w @md-to-pdf/markdown-echarts && npm run test -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run test -w @md-to-pdf/docx-engine && npm run test -w @md-to-pdf/renderer && npm run test -w @md-to-pdf/application && npm run build -w @md-to-pdf/application && npm run test -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/preview-engine && npm run test -w @md-to-pdf/web && npm run test -w @md-to-pdf/server && npm run test -w @md-to-pdf/desktop",
|
"test": "npm run test -w @md-to-pdf/markdown-echarts && npm run test -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run test -w @md-to-pdf/docx-theme-engine && npm run test -w @md-to-pdf/docx-engine && npm run test -w @md-to-pdf/renderer && npm run test -w @md-to-pdf/application && npm run build -w @md-to-pdf/application && npm run test -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/preview-engine && npm run test -w @md-to-pdf/web && npm run test -w @md-to-pdf/server && npm run test -w @md-to-pdf/desktop",
|
||||||
"typecheck": "npm run typecheck -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/markdown-echarts && npm run typecheck -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run typecheck -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/docx-engine && npm run typecheck -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/renderer && npm run typecheck -w @md-to-pdf/application && npm run build -w @md-to-pdf/application && npm run typecheck -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/preview-engine && npm run typecheck -w @md-to-pdf/web && npm run typecheck -w @md-to-pdf/server && npm run typecheck -w @md-to-pdf/desktop"
|
"typecheck": "npm run typecheck -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/markdown-echarts && npm run typecheck -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run typecheck -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/docx-theme-engine && npm run typecheck -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/docx-engine && npm run typecheck -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/renderer && npm run typecheck -w @md-to-pdf/application && npm run build -w @md-to-pdf/application && npm run typecheck -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/preview-engine && npm run typecheck -w @md-to-pdf/web && npm run typecheck -w @md-to-pdf/server && npm run typecheck -w @md-to-pdf/desktop"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
|
|||||||
@@ -27,6 +27,16 @@ export type DocxStylePreset = z.infer<
|
|||||||
typeof docxStylePresetSchema
|
typeof docxStylePresetSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export const docxThemeStyleModeSchema = z.enum([
|
||||||
|
"auto",
|
||||||
|
"auto-with-overrides",
|
||||||
|
"explicit"
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type DocxThemeStyleMode = z.infer<
|
||||||
|
typeof docxThemeStyleModeSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const docxFontFamilySchema = z.object({
|
export const docxFontFamilySchema = z.object({
|
||||||
latin: fontNameSchema,
|
latin: fontNameSchema,
|
||||||
eastAsia: fontNameSchema,
|
eastAsia: fontNameSchema,
|
||||||
@@ -37,7 +47,7 @@ export type DocxFontFamily = z.infer<
|
|||||||
typeof docxFontFamilySchema
|
typeof docxFontFamilySchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const docxBodyStyleSchema = z.object({
|
export const docxBodyStyleSchema = z.object({
|
||||||
fonts: docxFontFamilySchema.optional(),
|
fonts: docxFontFamilySchema.optional(),
|
||||||
sizePt: pointSizeSchema.optional(),
|
sizePt: pointSizeSchema.optional(),
|
||||||
color: colorSchema.optional(),
|
color: colorSchema.optional(),
|
||||||
@@ -47,7 +57,7 @@ const docxBodyStyleSchema = z.object({
|
|||||||
spacingAfterPt: spacingSchema.optional()
|
spacingAfterPt: spacingSchema.optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
const docxHeadingStyleSchema = z.object({
|
export const docxHeadingStyleSchema = z.object({
|
||||||
fonts: docxFontFamilySchema.optional(),
|
fonts: docxFontFamilySchema.optional(),
|
||||||
sizesPt: z
|
sizesPt: z
|
||||||
.tuple([
|
.tuple([
|
||||||
@@ -65,7 +75,7 @@ const docxHeadingStyleSchema = z.object({
|
|||||||
spacingAfterPt: spacingSchema.optional()
|
spacingAfterPt: spacingSchema.optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
const docxCodeStyleSchema = z.object({
|
export const docxCodeStyleSchema = z.object({
|
||||||
fonts: docxFontFamilySchema.optional(),
|
fonts: docxFontFamilySchema.optional(),
|
||||||
sizePt: pointSizeSchema.optional(),
|
sizePt: pointSizeSchema.optional(),
|
||||||
color: colorSchema.optional(),
|
color: colorSchema.optional(),
|
||||||
@@ -74,7 +84,7 @@ const docxCodeStyleSchema = z.object({
|
|||||||
lineSpacing: z.number().min(1).max(3).optional()
|
lineSpacing: z.number().min(1).max(3).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
const docxBlockStyleSchema = z.object({
|
export const docxBlockStyleSchema = z.object({
|
||||||
color: colorSchema.optional(),
|
color: colorSchema.optional(),
|
||||||
backgroundColor: colorSchema.optional(),
|
backgroundColor: colorSchema.optional(),
|
||||||
borderColor: colorSchema.optional(),
|
borderColor: colorSchema.optional(),
|
||||||
@@ -82,7 +92,7 @@ const docxBlockStyleSchema = z.object({
|
|||||||
italic: z.boolean().optional()
|
italic: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
const docxTableStyleSchema = z.object({
|
export const docxTableStyleSchema = z.object({
|
||||||
fonts: docxFontFamilySchema.optional(),
|
fonts: docxFontFamilySchema.optional(),
|
||||||
sizePt: pointSizeSchema.optional(),
|
sizePt: pointSizeSchema.optional(),
|
||||||
color: colorSchema.optional(),
|
color: colorSchema.optional(),
|
||||||
@@ -92,7 +102,7 @@ const docxTableStyleSchema = z.object({
|
|||||||
cellMarginMm: z.number().min(0).max(20).optional()
|
cellMarginMm: z.number().min(0).max(20).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
const docxCaptionStyleSchema = z.object({
|
export const docxCaptionStyleSchema = z.object({
|
||||||
fonts: docxFontFamilySchema.optional(),
|
fonts: docxFontFamilySchema.optional(),
|
||||||
sizePt: pointSizeSchema.optional(),
|
sizePt: pointSizeSchema.optional(),
|
||||||
color: colorSchema.optional(),
|
color: colorSchema.optional(),
|
||||||
@@ -100,8 +110,7 @@ const docxCaptionStyleSchema = z.object({
|
|||||||
alignment: z.enum(["left", "center", "right"]).optional()
|
alignment: z.enum(["left", "center", "right"]).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const docxThemeStyleSchema = z.object({
|
export const docxThemeStyleOverridesSchema = z.object({
|
||||||
preset: docxStylePresetSchema,
|
|
||||||
body: docxBodyStyleSchema.optional(),
|
body: docxBodyStyleSchema.optional(),
|
||||||
headings: docxHeadingStyleSchema.optional(),
|
headings: docxHeadingStyleSchema.optional(),
|
||||||
code: docxCodeStyleSchema.optional(),
|
code: docxCodeStyleSchema.optional(),
|
||||||
@@ -116,6 +125,18 @@ export const docxThemeStyleSchema = z.object({
|
|||||||
.optional()
|
.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<
|
export type DocxThemeStyle = z.infer<
|
||||||
typeof docxThemeStyleSchema
|
typeof docxThemeStyleSchema
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -209,4 +209,26 @@ describe("DOCX 主题样式协议", () => {
|
|||||||
})
|
})
|
||||||
).toThrow();
|
).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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -234,25 +234,45 @@ export function resolveDocxThemeStyle(
|
|||||||
preset: inferDocxStylePreset(manifest)
|
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 = {
|
const base = {
|
||||||
...baseStyle,
|
...baseStyle,
|
||||||
...preset
|
...preset
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
preset: declared.preset,
|
preset: presetName,
|
||||||
body: mergeSection(base.body, declared.body),
|
body: mergeSection(
|
||||||
headings: mergeSection(base.headings, declared.headings),
|
mergeSection(base.body, declared.body),
|
||||||
code: mergeSection(base.code, declared.code),
|
overrides?.body
|
||||||
|
),
|
||||||
|
headings: mergeSection(
|
||||||
|
mergeSection(base.headings, declared.headings),
|
||||||
|
overrides?.headings
|
||||||
|
),
|
||||||
|
code: mergeSection(
|
||||||
|
mergeSection(base.code, declared.code),
|
||||||
|
overrides?.code
|
||||||
|
),
|
||||||
blockQuote: mergeSection(
|
blockQuote: mergeSection(
|
||||||
base.blockQuote,
|
mergeSection(base.blockQuote, declared.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(
|
hyperlink: mergeSection(
|
||||||
base.hyperlink,
|
mergeSection(base.hyperlink, declared.hyperlink),
|
||||||
declared.hyperlink
|
overrides?.hyperlink
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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。
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./configuration.js";
|
||||||
|
export * from "./slots.js";
|
||||||
|
export * from "./snapshot.js";
|
||||||
|
export * from "./tokens.js";
|
||||||
@@ -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
|
||||||
|
>;
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user