统一 Chromium 与 DOCX 的受限高度封面布局,补齐可编辑边框、装饰边和 Office 页面舍入适配。 按四套独立封面主题、纵横方向及五组页边距执行 40 个 Word/WPS 真实视觉场景,严格失败为 0,并保留正文诊断供 D4 修复。
174 lines
4.8 KiB
TypeScript
174 lines
4.8 KiB
TypeScript
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(),
|
|
outline: docxBorderTokenSchema.optional(),
|
|
outlineOffsetPt: z.number().min(-1000).max(1000).optional(),
|
|
widthPercent: z.number().min(0).max(100).optional(),
|
|
minimumWidthPercent: z.number().min(0).max(100).optional(),
|
|
minimumHeightPt: z.number().min(0).max(10000).optional(),
|
|
contentPaddingLeftPt: z.number().min(0).max(1000).optional(),
|
|
verticalAlignment: z
|
|
.enum(["top", "center", "bottom"])
|
|
.optional(),
|
|
childAlignment: z
|
|
.enum(["left", "center", "right", "stretch"])
|
|
.optional(),
|
|
selfAlignment: z
|
|
.enum(["left", "center", "right", "stretch"])
|
|
.optional(),
|
|
hidden: z.boolean().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
|
|
>;
|