新增通用 CSS 到 OOXML 翻译修复,统一字体、字距、精确行距、段落、列表、表格、引用、代码块与行内代码连续性,不引入按主题 ID 分支。 新增 MdTP Mono 并统一 Serif、Sans、Mono 三字体包的 Chromium 与 DOCX 使用链;字体声明、嵌入部件和 Word/WPS 实际采用均进入硬门禁。 重建封面整页及正文语义块视觉差分,14 套主题、纵横两个方向、五组页边距共 140 个真实场景全部通过,阻断失败和诊断失败均为零。 源码服务、Docker Web API 与实际安装 Desktop 的 red-briefing 导出均包含 5 个字体部件;Word/WPS 原生渲染和逐页复核通过。修复 Docker 构建上下文与运行层复用软链接,并完善 v0.6.1 版本、发行说明和发布归集。 验证:npm test(116 个文件、616 项测试)、npm run typecheck、npm run build、git diff --check 全部通过。Desktop 安装器与 ZIP、Docker v0.6.1 镜像已生成;Windows 产物仍为未签名内部发行。
335 lines
9.3 KiB
TypeScript
335 lines
9.3 KiB
TypeScript
import { z } from "zod";
|
|
import { exportConfigSchema } from "./export-config.js";
|
|
|
|
export const DOCX_PANDOC_VERSION = "3.9.0.2";
|
|
export const DOCX_MIME_TYPE =
|
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
export const DOCX_FILE_EXTENSION = ".docx";
|
|
export const MAXIMUM_DOCX_MARKDOWN_LENGTH = 1_500_000;
|
|
export const MAXIMUM_DOCX_FILE_NAME_LENGTH = 500;
|
|
export const MAXIMUM_DOCX_RESOURCE_COUNT = 50;
|
|
export const DOCX_MEDIA_RASTER_DPI = 300;
|
|
export const DOCX_MEDIA_CSS_DPI = 96;
|
|
export const DOCX_MEDIA_RASTER_SCALE =
|
|
DOCX_MEDIA_RASTER_DPI / DOCX_MEDIA_CSS_DPI;
|
|
export const MAXIMUM_DOCX_MEDIA_EDGE_PIXELS = 4_096;
|
|
export const MAXIMUM_DOCX_MEDIA_PIXELS = 16_000_000;
|
|
export const MAXIMUM_DOCX_MEDIA_BYTES = 8 * 1024 * 1024;
|
|
export const MAXIMUM_DOCX_TOTAL_MEDIA_BYTES = 30 * 1024 * 1024;
|
|
export const MAXIMUM_DOCX_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
|
|
export const documentExportFormatSchema = z.enum(["pdf", "docx"]);
|
|
export type DocumentExportFormat = z.infer<
|
|
typeof documentExportFormatSchema
|
|
>;
|
|
|
|
export const docxSourceResourceSchema = z.object({
|
|
path: z.string().min(1).max(1_000),
|
|
contentType: z.string().min(1).max(100).optional(),
|
|
data: z.string().min(1)
|
|
});
|
|
|
|
export type DocxSourceResource = z.infer<
|
|
typeof docxSourceResourceSchema
|
|
>;
|
|
|
|
export const docxExportRequestSchema = z.object({
|
|
markdown: z.string().max(MAXIMUM_DOCX_MARKDOWN_LENGTH),
|
|
fileName: z
|
|
.string()
|
|
.max(MAXIMUM_DOCX_FILE_NAME_LENGTH)
|
|
.default("文档.md"),
|
|
language: z.string().max(50).default("zh-CN"),
|
|
resources: z
|
|
.array(docxSourceResourceSchema)
|
|
.max(MAXIMUM_DOCX_RESOURCE_COUNT)
|
|
.default([]),
|
|
exportConfig: exportConfigSchema
|
|
});
|
|
|
|
export type DocxExportRequest = z.infer<
|
|
typeof docxExportRequestSchema
|
|
>;
|
|
export type DocxExportRequestInput = z.input<
|
|
typeof docxExportRequestSchema
|
|
>;
|
|
|
|
export const docxRuntimeStatusSchema = z.enum([
|
|
"available",
|
|
"not-found",
|
|
"version-mismatch",
|
|
"not-executable",
|
|
"probe-timeout"
|
|
]);
|
|
|
|
export type DocxRuntimeStatus = z.infer<
|
|
typeof docxRuntimeStatusSchema
|
|
>;
|
|
|
|
const availableDocxCapabilitySchema = z.object({
|
|
format: z.literal("docx"),
|
|
status: z.literal("available"),
|
|
expectedVersion: z.literal(DOCX_PANDOC_VERSION),
|
|
detectedVersion: z.literal(DOCX_PANDOC_VERSION)
|
|
});
|
|
|
|
const unavailableDocxCapabilitySchema = z.object({
|
|
format: z.literal("docx"),
|
|
status: docxRuntimeStatusSchema.exclude(["available"]),
|
|
expectedVersion: z.literal(DOCX_PANDOC_VERSION),
|
|
detectedVersion: z.string().max(100).optional(),
|
|
message: z.string().max(500)
|
|
});
|
|
|
|
export const docxCapabilitySchema = z.discriminatedUnion("status", [
|
|
availableDocxCapabilitySchema,
|
|
unavailableDocxCapabilitySchema
|
|
]);
|
|
|
|
export type DocxCapability = z.infer<typeof docxCapabilitySchema>;
|
|
|
|
export const docxMediaKindSchema = z.enum([
|
|
"image",
|
|
"mermaid",
|
|
"echarts"
|
|
]);
|
|
|
|
export type DocxMediaKind = z.infer<typeof docxMediaKindSchema>;
|
|
|
|
export const docxMediaAlignmentSchema = z.enum([
|
|
"left",
|
|
"center",
|
|
"right"
|
|
]);
|
|
|
|
export type DocxMediaAlignment = z.infer<
|
|
typeof docxMediaAlignmentSchema
|
|
>;
|
|
|
|
export const docxTableLayoutSchema = z.object({
|
|
ordinal: z.number().int().min(1).max(256),
|
|
widthPercent: z.number().positive().max(100),
|
|
leftOffsetPercent: z.number().min(0).max(100),
|
|
columnWidthPercents: z
|
|
.array(z.number().positive().max(100))
|
|
.min(1)
|
|
.max(64),
|
|
rows: z
|
|
.array(
|
|
z.object({
|
|
cells: z
|
|
.array(
|
|
z.object({
|
|
columnSpan: z.number().int().min(1).max(64),
|
|
backgroundColor: z
|
|
.string()
|
|
.regex(/^#[0-9a-f]{6}$/iu),
|
|
color: z.string().regex(/^#[0-9a-f]{6}$/iu),
|
|
bold: z.boolean(),
|
|
italic: z.boolean(),
|
|
alignment: z.enum(["left", "center", "right", "justify"])
|
|
})
|
|
)
|
|
.min(1)
|
|
.max(64)
|
|
})
|
|
)
|
|
.max(2048)
|
|
});
|
|
|
|
export type DocxTableLayout = z.infer<
|
|
typeof docxTableLayoutSchema
|
|
>;
|
|
|
|
export const docxTextBlockLayoutSchema = z.object({
|
|
ordinal: z.number().int().min(1).max(100_000),
|
|
text: z.string().min(1).max(100_000),
|
|
letterSpacingPt: z.number().min(-20).max(100),
|
|
alignment: z
|
|
.enum(["left", "center", "right", "justify", "distribute"])
|
|
.optional(),
|
|
linePitchPt: z.number().min(1).max(500).optional(),
|
|
lineBreakOffsets: z
|
|
.array(z.number().int().min(1).max(100_000))
|
|
.max(10_000)
|
|
});
|
|
|
|
export type DocxTextBlockLayout = z.infer<
|
|
typeof docxTextBlockLayoutSchema
|
|
>;
|
|
|
|
export const docxListItemLayoutSchema = z.object({
|
|
ordinal: z.number().int().min(1).max(100_000),
|
|
text: z.string().min(1).max(100_000),
|
|
depth: z.number().int().min(0).max(64),
|
|
textStartPt: z.number().min(0).max(2_000)
|
|
});
|
|
|
|
export type DocxListItemLayout = z.infer<
|
|
typeof docxListItemLayoutSchema
|
|
>;
|
|
|
|
export const docxInlineCodeLayoutSchema = z.object({
|
|
ordinal: z.number().int().min(1).max(100_000),
|
|
text: z.string().min(1).max(100_000),
|
|
fontSizePt: z.number().min(1).max(200),
|
|
letterSpacingPt: z.number().min(-20).max(100),
|
|
color: z.string().regex(/^#[0-9a-f]{6}$/iu),
|
|
backgroundColor: z.string().regex(/^#[0-9a-f]{6}$/iu).optional(),
|
|
paddingPt: z.object({
|
|
top: z.number().min(0).max(1_000),
|
|
right: z.number().min(0).max(1_000),
|
|
bottom: z.number().min(0).max(1_000),
|
|
left: z.number().min(0).max(1_000)
|
|
}),
|
|
borderPt: z.object({
|
|
top: z.number().min(0).max(20),
|
|
right: z.number().min(0).max(20),
|
|
bottom: z.number().min(0).max(20),
|
|
left: z.number().min(0).max(20)
|
|
})
|
|
});
|
|
|
|
export type DocxInlineCodeLayout = z.infer<
|
|
typeof docxInlineCodeLayoutSchema
|
|
>;
|
|
|
|
export const docxDocumentLayoutPlanSchema = z.object({
|
|
tables: z.array(docxTableLayoutSchema).max(256),
|
|
textBlocks: z.array(docxTextBlockLayoutSchema).max(100_000).optional(),
|
|
listItems: z.array(docxListItemLayoutSchema).max(100_000).optional(),
|
|
inlineCodes: z.array(docxInlineCodeLayoutSchema).max(100_000).optional()
|
|
});
|
|
|
|
export type DocxDocumentLayoutPlan = z.infer<
|
|
typeof docxDocumentLayoutPlanSchema
|
|
>;
|
|
|
|
export const docxMediaCaptureTargetSchema = z.object({
|
|
id: z.string().regex(/^docx-media-\d+$/u),
|
|
kind: docxMediaKindSchema,
|
|
ordinal: z.number().int().min(1).max(MAXIMUM_DOCX_RESOURCE_COUNT),
|
|
kindOrdinal: z
|
|
.number()
|
|
.int()
|
|
.min(1)
|
|
.max(MAXIMUM_DOCX_RESOURCE_COUNT),
|
|
altText: z.string().max(1_000),
|
|
caption: z.string().max(1_000).optional(),
|
|
alignment: docxMediaAlignmentSchema,
|
|
displayWidthPx: z.number().positive().max(10_000),
|
|
displayHeightPx: z.number().positive().max(10_000),
|
|
captureX: z.number().int().nonnegative().max(100_000),
|
|
captureY: z.number().int().nonnegative().max(100_000),
|
|
captureWidthPx: z.number().int().positive().max(10_000),
|
|
captureHeightPx: z.number().int().positive().max(10_000),
|
|
rasterScale: z.number().positive().max(DOCX_MEDIA_RASTER_SCALE)
|
|
});
|
|
|
|
export type DocxMediaCaptureTarget = z.infer<
|
|
typeof docxMediaCaptureTargetSchema
|
|
>;
|
|
|
|
export const docxMediaCapturePlanSchema = z.object({
|
|
targets: z
|
|
.array(docxMediaCaptureTargetSchema)
|
|
.max(MAXIMUM_DOCX_RESOURCE_COUNT),
|
|
echartsErrors: z.array(z.string().max(1_000)),
|
|
mermaidErrors: z.array(z.string().max(1_000)),
|
|
documentLayout: docxDocumentLayoutPlanSchema.optional()
|
|
});
|
|
|
|
export type DocxMediaCapturePlan = z.infer<
|
|
typeof docxMediaCapturePlanSchema
|
|
>;
|
|
|
|
export interface DocxPngMediaResource
|
|
extends DocxMediaCaptureTarget {
|
|
fileName: string;
|
|
contentType: "image/png";
|
|
content: Uint8Array;
|
|
pixelWidth: number;
|
|
pixelHeight: number;
|
|
}
|
|
|
|
export interface PreparedDocxMedia {
|
|
resources: DocxPngMediaResource[];
|
|
echartsErrors: string[];
|
|
mermaidErrors: string[];
|
|
warnings: string[];
|
|
totalBytes: number;
|
|
documentLayout?: DocxDocumentLayoutPlan;
|
|
}
|
|
|
|
export const docxExportErrorCodeSchema = z.enum([
|
|
"INVALID_DOCX_REQUEST",
|
|
"INVALID_EXPORT_CONFIG",
|
|
"INVALID_FILE_NAME",
|
|
"INVALID_MARKDOWN",
|
|
"INVALID_FRONT_MATTER",
|
|
"MARKDOWN_TOO_LARGE",
|
|
"INVALID_IMAGE_RESOURCES",
|
|
"IMAGE_RESOURCES_TOO_LARGE",
|
|
"THEME_NOT_FOUND",
|
|
"DOCX_RUNTIME_NOT_FOUND",
|
|
"DOCX_RUNTIME_VERSION_MISMATCH",
|
|
"DOCX_RUNTIME_NOT_EXECUTABLE",
|
|
"DOCX_RUNTIME_PROBE_TIMEOUT",
|
|
"DOCX_QUEUE_FULL",
|
|
"DOCX_RENDER_TIMEOUT",
|
|
"DOCX_GENERATION_FAILED",
|
|
"DOCX_OUTPUT_INVALID"
|
|
]);
|
|
|
|
export type DocxExportErrorCode = z.infer<
|
|
typeof docxExportErrorCodeSchema
|
|
>;
|
|
|
|
export const docxExportErrorResponseSchema = z.object({
|
|
error: docxExportErrorCodeSchema,
|
|
message: z.string().min(1).max(1_000),
|
|
retryable: z.boolean().default(false)
|
|
});
|
|
|
|
export type DocxExportErrorResponse = z.infer<
|
|
typeof docxExportErrorResponseSchema
|
|
>;
|
|
|
|
export interface DocxGenerationTimings {
|
|
queueMs: number;
|
|
probeMs: number;
|
|
prepareMs: number;
|
|
themeStyleMs: number;
|
|
mediaMs: number;
|
|
referenceMs: number;
|
|
pandocMs: number;
|
|
validationMs: number;
|
|
totalMs: number;
|
|
}
|
|
|
|
export interface DocxExportDiagnostics {
|
|
warnings: string[];
|
|
echartsErrors: string[];
|
|
mermaidErrors: string[];
|
|
}
|
|
|
|
export interface DocxExportResult {
|
|
docx: Uint8Array;
|
|
fileName: string;
|
|
diagnostics: DocxExportDiagnostics;
|
|
timings: DocxGenerationTimings;
|
|
}
|
|
|
|
export function createDocxFileName(fileName: unknown) {
|
|
const source =
|
|
typeof fileName === "string"
|
|
? fileName.split(/[\\/]/).at(-1) ?? ""
|
|
: "";
|
|
const stem = source
|
|
.replace(/\.(?:md|markdown|docx)$/i, "")
|
|
.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "_")
|
|
.replace(/[.\s]+$/g, "")
|
|
.trim();
|
|
return `${stem || "文档"}${DOCX_FILE_EXTENSION}`;
|
|
}
|