import { z } from "zod/mini" import { ImageConstraintSchema } from "./image" export const RichTextFieldType = "StructuredText" export const DEFAULT_OPTION = "paragraph" export const RichTextModelNodeType = { heading1: "heading1", heading2: "heading2", heading3: "heading3", heading4: "heading4", heading5: "heading5", heading6: "heading6", paragraph: "paragraph", preformatted: "preformatted", orderedList: "o-list-item", list: "list-item", image: "image", embed: "embed", hyperlink: "hyperlink", strong: "strong", em: "em", rtl: "rtl", } as const export type RichTextModelNodeTypes = (typeof RichTextModelNodeType)[keyof typeof RichTextModelNodeType] const VALID_NODE_TYPES = Object.values(RichTextModelNodeType) /** * RichTextOptions parses comma-separated node types, filters invalid ones, * and defaults to "paragraph" if empty or null. */ const RichTextOptionsSchema = z.pipe( z.union([z.string(), z.null()]), z.transform((s: string | null) => { if (!s) return DEFAULT_OPTION const entries = s.split(",").map((e: string) => e.trim()) const filtered = entries.filter((e: string) => VALID_NODE_TYPES.includes(e as (typeof VALID_NODE_TYPES)[number]), ) return filtered.length ? filtered.join(",") : DEFAULT_OPTION }), ) /** * RichTextLabels accepts multiple formats and normalizes to string[]: * - null → [] * - "label" → ["label"] * - ["a", "b"] → ["a", "b"] * - { "": [{name: "a"}], "key": [{name: "b"}] } → ["a", "b"] (legacy format) */ const LegacyLabelsFormat = z.record(z.string(), z.array(z.object({ name: z.string() }))) const RichTextLabelsSchema: z.ZodMiniType = z.pipe( z.union([ z.pipe( z.null(), z.transform(() => [] as string[]), ), z.pipe( z.string(), z.transform((s: string) => [s]), ), z.array(z.string()), z.pipe( LegacyLabelsFormat, z.transform((obj: Record>) => { const entries = Object.entries(obj) if (!entries.length) return [] as string[] // Handle legacy format with empty key if (obj[""]) { return obj[""].map((l: { name: string }) => l.name) } // Convert all entries to flat array of names return entries .reduce((acc, [, labelsEntries]) => { return acc.concat(labelsEntries.map((l: { name: string }) => l.name)) }, []) .filter(Boolean) }), ), ]), z.transform((result: string[]) => result as string[]), ) const RichTextConfigSchema = z.object({ label: z.nullish(z.string()), placeholder: z.optional(z.string()), useAsTitle: z.optional(z.boolean()), single: z.optional(RichTextOptionsSchema), multi: z.optional(RichTextOptionsSchema), imageConstraint: z.optional(ImageConstraintSchema), labels: z.optional(RichTextLabelsSchema), allowTargetBlank: z.optional(z.boolean()), }) export const RichTextModelSchema = z.object({ type: z.literal(RichTextFieldType), fieldset: z.nullish(z.string()), config: z.optional(RichTextConfigSchema), }) export type RichTextModel = z.infer