import { OfficeError } from "../core/errors.ts"; export type StylePreset = "editorial" | "business" | "formal"; export interface MarginsCm { top: number; bottom: number; left: number; right: number; } export interface WordStyle { preset: StylePreset; label: string; chineseBodyFont: string; latinBodyFont: string; headingFont: string; bodySize: number; titleSize: number; heading1Size: number; heading2Size: number; heading3Size: number; lineSpacing: number; firstLineIndent: number; margins: MarginsCm; accentColor: string; headerFooter: boolean; } export interface WordStyleInput { chineseBodyFont?: string; latinBodyFont?: string; headingFont?: string; bodySize?: number; titleSize?: number; heading1Size?: number; heading2Size?: number; heading3Size?: number; lineSpacing?: number; firstLineIndent?: number; margins?: Partial; accentColor?: string; headerFooter?: boolean; } const PRESETS: Record> = { editorial: { label: "白色编辑风", chineseBodyFont: "SimSun", latinBodyFont: "Calibri", headingFont: "Microsoft YaHei", bodySize: 12.5, titleSize: 32, heading1Size: 26, heading2Size: 15, heading3Size: 13.5, lineSpacing: 1.5, firstLineIndent: 0, margins: { top: 2.15, bottom: 1.9, left: 2.65, right: 2.65 }, accentColor: "A63A32", headerFooter: true, }, business: { label: "商务报告风", chineseBodyFont: "Microsoft YaHei", latinBodyFont: "Calibri", headingFont: "Microsoft YaHei", bodySize: 11.5, titleSize: 28, heading1Size: 20, heading2Size: 14, heading3Size: 12.5, lineSpacing: 1.45, firstLineIndent: 0, margins: { top: 2.54, bottom: 2.54, left: 2.54, right: 2.54 }, accentColor: "1F4E79", headerFooter: true, }, formal: { label: "正式公文/合同风", chineseBodyFont: "SimSun", latinBodyFont: "Times New Roman", headingFont: "SimSun", bodySize: 12, titleSize: 18, heading1Size: 16, heading2Size: 14, heading3Size: 12, lineSpacing: 1.5, firstLineIndent: 2, margins: { top: 2.54, bottom: 2.54, left: 2.54, right: 2.54 }, accentColor: "252525", headerFooter: false, }, }; function cloneStyle(preset: StylePreset): WordStyle { const style = PRESETS[preset]; return { preset, ...style, margins: { ...style.margins } }; } function requireFont(value: string | undefined, label: string): void { if (value !== undefined && !value.trim()) throw new OfficeError(`${label}不能为空。`, "INVALID_STYLE"); } function requireNumber(value: number | undefined, label: string, minimum: number, maximum: number): void { if (value !== undefined && (!Number.isFinite(value) || value < minimum || value > maximum)) { throw new OfficeError(`${label}必须介于 ${minimum} 和 ${maximum} 之间。`, "INVALID_STYLE"); } } function requireColor(value: string | undefined): void { if (value !== undefined && !/^[0-9A-Fa-f]{6}$/.test(value)) { throw new OfficeError("强调色必须是六位十六进制颜色,例如 1F4E79。", "INVALID_STYLE"); } } export function resolveWordStyle( documentType: "general" | "report" | "contract" | "news", preset?: StylePreset, custom?: WordStyleInput, ): WordStyle { const basePreset = preset ?? (documentType === "contract" ? "formal" : "editorial"); const style = cloneStyle(basePreset); if (!custom) return style; requireFont(custom.chineseBodyFont, "中文正文字体"); requireFont(custom.latinBodyFont, "英文正文字体"); requireFont(custom.headingFont, "标题字体"); requireNumber(custom.bodySize, "正文字号", 8, 24); requireNumber(custom.titleSize, "标题字号", 12, 48); requireNumber(custom.heading1Size, "一级标题字号", 10, 40); requireNumber(custom.heading2Size, "二级标题字号", 9, 32); requireNumber(custom.heading3Size, "三级标题字号", 8, 28); requireNumber(custom.lineSpacing, "行距倍数", 1, 3); requireNumber(custom.firstLineIndent, "首行缩进", 0, 4); requireColor(custom.accentColor); for (const [edge, value] of Object.entries(custom.margins ?? {})) requireNumber(value, `${edge}页边距`, 0.5, 5); return { ...style, ...(custom.chineseBodyFont !== undefined ? { chineseBodyFont: custom.chineseBodyFont.trim() } : {}), ...(custom.latinBodyFont !== undefined ? { latinBodyFont: custom.latinBodyFont.trim() } : {}), ...(custom.headingFont !== undefined ? { headingFont: custom.headingFont.trim() } : {}), ...(custom.bodySize !== undefined ? { bodySize: custom.bodySize } : {}), ...(custom.titleSize !== undefined ? { titleSize: custom.titleSize } : {}), ...(custom.heading1Size !== undefined ? { heading1Size: custom.heading1Size } : {}), ...(custom.heading2Size !== undefined ? { heading2Size: custom.heading2Size } : {}), ...(custom.heading3Size !== undefined ? { heading3Size: custom.heading3Size } : {}), ...(custom.lineSpacing !== undefined ? { lineSpacing: custom.lineSpacing } : {}), ...(custom.firstLineIndent !== undefined ? { firstLineIndent: custom.firstLineIndent } : {}), ...(custom.accentColor !== undefined ? { accentColor: custom.accentColor.toUpperCase() } : {}), ...(custom.headerFooter !== undefined ? { headerFooter: custom.headerFooter } : {}), margins: { ...style.margins, ...(custom.margins ?? {}) }, }; } export const STYLE_PRESET_CHOICES: ReadonlyArray<{ preset: StylePreset; label: string; summary: string }> = [ { preset: "editorial", label: "白色编辑风", summary: "留白充足、暗红强调,适合文章、说明和新闻稿" }, { preset: "business", label: "商务报告风", summary: "深蓝强调、清晰层级,适合汇报、方案和周报" }, { preset: "formal", label: "正式公文/合同风", summary: "宋体为主、保守排版,适合合同、通知和函件" }, ];