feat: 实现动态 DOCX 模板与样式映射

This commit is contained in:
SkyJourney
2026-07-30 14:10:43 +08:00
parent fe5d67fb6d
commit fa159d916f
41 changed files with 3610 additions and 7 deletions
+1
View File
@@ -10,6 +10,7 @@ src/
document.ts Markdown 文档、分页载荷、结果与耗时模型
document-link.ts 跨端链接分类与 PDF 本地链接编码协议
docx.ts DOCX 请求、能力、错误、结果与文件名协议
docx-style.ts 主题 DOCX 样式预设和安全覆盖协议
export-config.ts 纸张、边距、页眉页脚、页码与图表配置
theme.ts 主题清单、主题能力与 CSS 载荷模型
index.ts 公共导出入口
+121
View File
@@ -0,0 +1,121 @@
import { z } from "zod";
const colorSchema = z
.string()
.regex(/^#[0-9a-f]{6}$/iu, "DOCX 使");
const fontNameSchema = z
.string()
.trim()
.min(1)
.max(100)
.regex(
/^[\p{L}\p{N}\s._-]+$/u,
"DOCX 字体名称包含不支持的字符"
);
const pointSizeSchema = z.number().min(6).max(72);
const spacingSchema = z.number().min(0).max(144);
export const docxStylePresetSchema = z.enum([
"general",
"technical",
"official",
"formal",
"tender"
]);
export type DocxStylePreset = z.infer<
typeof docxStylePresetSchema
>;
export const docxFontFamilySchema = z.object({
latin: fontNameSchema,
eastAsia: fontNameSchema,
complexScript: fontNameSchema.optional()
});
export type DocxFontFamily = z.infer<
typeof docxFontFamilySchema
>;
const docxBodyStyleSchema = z.object({
fonts: docxFontFamilySchema.optional(),
sizePt: pointSizeSchema.optional(),
color: colorSchema.optional(),
lineSpacing: z.number().min(1).max(3).optional(),
firstLineIndentChars: z.number().min(0).max(10).optional(),
spacingBeforePt: spacingSchema.optional(),
spacingAfterPt: spacingSchema.optional()
});
const docxHeadingStyleSchema = z.object({
fonts: docxFontFamilySchema.optional(),
sizesPt: z
.tuple([
pointSizeSchema,
pointSizeSchema,
pointSizeSchema,
pointSizeSchema,
pointSizeSchema,
pointSizeSchema
])
.optional(),
color: colorSchema.optional(),
bold: z.boolean().optional(),
spacingBeforePt: spacingSchema.optional(),
spacingAfterPt: spacingSchema.optional()
});
const docxCodeStyleSchema = z.object({
fonts: docxFontFamilySchema.optional(),
sizePt: pointSizeSchema.optional(),
color: colorSchema.optional(),
backgroundColor: colorSchema.optional(),
borderColor: colorSchema.optional(),
lineSpacing: z.number().min(1).max(3).optional()
});
const docxBlockStyleSchema = z.object({
color: colorSchema.optional(),
backgroundColor: colorSchema.optional(),
borderColor: colorSchema.optional(),
leftIndentChars: z.number().min(0).max(20).optional(),
italic: z.boolean().optional()
});
const docxTableStyleSchema = z.object({
fonts: docxFontFamilySchema.optional(),
sizePt: pointSizeSchema.optional(),
color: colorSchema.optional(),
headerColor: colorSchema.optional(),
headerBackgroundColor: colorSchema.optional(),
borderColor: colorSchema.optional(),
cellMarginMm: z.number().min(0).max(20).optional()
});
const docxCaptionStyleSchema = z.object({
fonts: docxFontFamilySchema.optional(),
sizePt: pointSizeSchema.optional(),
color: colorSchema.optional(),
italic: z.boolean().optional(),
alignment: z.enum(["left", "center", "right"]).optional()
});
export const docxThemeStyleSchema = z.object({
preset: docxStylePresetSchema,
body: docxBodyStyleSchema.optional(),
headings: docxHeadingStyleSchema.optional(),
code: docxCodeStyleSchema.optional(),
blockQuote: docxBlockStyleSchema.optional(),
table: docxTableStyleSchema.optional(),
caption: docxCaptionStyleSchema.optional(),
hyperlink: z
.object({
color: colorSchema.optional(),
underline: z.boolean().optional()
})
.optional()
});
export type DocxThemeStyle = z.infer<
typeof docxThemeStyleSchema
>;
+1
View File
@@ -1,5 +1,6 @@
export * from "./document.js";
export * from "./docx.js";
export * from "./docx-style.js";
export * from "./document-profile.js";
export * from "./document-link.js";
export * from "./export-config.js";
+2
View File
@@ -1,5 +1,6 @@
import { z } from "zod";
import { documentProfileNameSchema } from "./document-profile.js";
import { docxThemeStyleSchema } from "./docx-style.js";
import {
footerSchema,
headerSchema,
@@ -50,6 +51,7 @@ export const themeManifestSchema = z.object({
footer: footerSchema.optional()
})
.optional(),
docxStyle: docxThemeStyleSchema.optional(),
bundled: z.boolean()
});
+46 -1
View File
@@ -10,7 +10,8 @@ import {
docxCapabilitySchema,
docxMediaCapturePlanSchema,
docxExportErrorResponseSchema,
docxExportRequestSchema
docxExportRequestSchema,
docxThemeStyleSchema
} from "../src/index.js";
describe("DOCX 共享协议", () => {
@@ -165,3 +166,47 @@ describe("DOCX 共享协议", () => {
).toBe(false);
});
});
describe("DOCX 主题样式协议", () => {
it("接受预设和受限样式覆盖", () => {
expect(
docxThemeStyleSchema.parse({
preset: "formal",
body: {
fonts: {
latin: "Times New Roman",
eastAsia: "SimSun"
},
sizePt: 12,
lineSpacing: 1.5
},
headings: {
sizesPt: [22, 18, 16, 14, 12, 10.5],
color: "#1f2937"
},
table: {
borderColor: "#d1d5db",
cellMarginMm: 1.5
}
})
).toMatchObject({
preset: "formal",
body: { sizePt: 12, lineSpacing: 1.5 }
});
});
it("拒绝危险字体名称和越界样式值", () => {
expect(() =>
docxThemeStyleSchema.parse({
preset: "general",
body: {
fonts: {
latin: "Arial; DROP",
eastAsia: "宋体"
},
sizePt: 200
}
})
).toThrow();
});
});