feat: 建立 DOCX 主题映射引擎
This commit is contained in:
@@ -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
|
||||
>;
|
||||
Reference in New Issue
Block a user