Files
MorphDoc/packages/core/src/theme.ts
T

129 lines
3.2 KiB
TypeScript

import { z } from "zod";
import { documentProfileNameSchema } from "./document-profile.js";
import { docxThemeStyleSchema } from "./docx-style.js";
import {
footerSchema,
headerSchema,
pageMarginsSchema
} from "./export-config.js";
export const THEME_MANIFEST_VERSION = 1;
export const MAXIMUM_DOCX_FONT_FACE_COUNT = 16;
export const MAXIMUM_DOCX_FONT_SOURCE_BYTES = 8 * 1024 * 1024;
export const MAXIMUM_DOCX_FONT_TOTAL_SOURCE_BYTES =
32 * 1024 * 1024;
const docxFontNameSchema = z
.string()
.trim()
.min(1)
.max(100)
.regex(
/^[\p{L}\p{N}\s._-]+$/u,
"DOCX 字体名称包含不支持的字符"
);
const docxFontSourceSchema = z
.string()
.trim()
.min(1)
.max(500)
.refine(
(value) =>
!value.includes("\0") &&
!value.startsWith("/") &&
!value.startsWith("\\") &&
!/^[a-z]:[\\/]/iu.test(value) &&
!/^(?:https?:)?\/\//iu.test(value) &&
!value
.replace(/^theme-shared:/u, "")
.split(/[\\/]/u)
.includes(".."),
"DOCX 字体资源路径不安全"
)
.refine(
(value) => /\.(?:otf|ttf|woff2?)$/iu.test(value),
"DOCX 字体资源格式必须为 OTF、TTF、WOFF 或 WOFF2"
);
export const docxFontFaceSchema = z.object({
family: docxFontNameSchema,
aliases: z
.array(docxFontNameSchema)
.max(16)
.default([]),
source: docxFontSourceSchema,
weight: z
.number()
.int()
.min(100)
.max(900)
.multipleOf(100)
.default(400),
style: z.enum(["normal", "italic"]).default("normal"),
license: z.string().trim().min(1).max(200)
});
export const docxFontManifestSchema = z.object({
faces: z
.array(docxFontFaceSchema)
.min(1)
.max(MAXIMUM_DOCX_FONT_FACE_COUNT)
});
export type DocxFontFace = z.infer<typeof docxFontFaceSchema>;
export type DocxFontManifest = z.infer<
typeof docxFontManifestSchema
>;
export const themeFeatureSchema = z.enum([
"code",
"table",
"task-list",
"footnote",
"katex",
"mermaid",
"echarts"
]);
export const themeCategorySchema = z.enum([
"general",
"red-letter",
"formal",
"tender"
]);
export const themeManifestSchema = z.object({
manifestVersion: z.literal(THEME_MANIFEST_VERSION),
id: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
name: z.string().min(1).max(100),
version: z.string().min(1).max(30),
description: z.string().max(500),
author: z.string().max(100),
license: z.string().max(100),
base: z.string().endsWith(".css").optional(),
entry: z.string().endsWith(".css"),
print: z.string().endsWith(".css").optional(),
domPreset: z.enum(["typora", "github", "generic"]),
defaultFontSize: z.string(),
supportedFeatures: z.array(themeFeatureSchema),
category: themeCategorySchema.default("general"),
compatibleProfiles: z
.array(documentProfileNameSchema)
.default([]),
pageDefaults: z
.object({
margins: pageMarginsSchema,
header: headerSchema.optional(),
footer: footerSchema.optional()
})
.optional(),
docxFonts: docxFontManifestSchema.optional(),
docxStyle: docxThemeStyleSchema.optional(),
bundled: z.boolean()
});
export type ThemeManifest = z.infer<typeof themeManifestSchema>;
export type ThemeFeature = z.infer<typeof themeFeatureSchema>;
export type ThemeCategory = z.infer<typeof themeCategorySchema>;