feat: 实现动态 DOCX 模板与样式映射
This commit is contained in:
@@ -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 公共导出入口
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user