import { z } from "zod/mini" // It's important to only import the types here to // avoid runtime circular dependencies! import type { EmbedContentSchema } from "../embed" import type { ImageContentViewSchema } from "../image" import type { FilledLinkContentValue, FilledLinkContentValueSchema } from "../link" import { EmbedLegacyLooseSchema } from "./embed" import { ImageLegacyViewSchema } from "./image" import { FilledLinkLegacyValueSchema } from "./link" import type { FilledLinkLegacyValue } from "./link" export const RichTextNodeType = { heading1: "heading1", heading2: "heading2", heading3: "heading3", heading4: "heading4", heading5: "heading5", heading6: "heading6", paragraph: "paragraph", preformatted: "preformatted", oListItem: "o-list-item", listItem: "list-item", image: "image", embed: "embed", hyperlink: "hyperlink", strong: "strong", em: "em", } as const export type RichTextNodeTypes = (typeof RichTextNodeType)[keyof typeof RichTextNodeType] // Spans type HyperlinkSpan = { type: typeof RichTextNodeType.hyperlink start: number end: number data: TData } const getHyperlinkSpanSchema = < TData extends typeof FilledLinkLegacyValueSchema | typeof FilledLinkContentValueSchema, >( data: TData, ) => z.object({ type: z.literal(RichTextNodeType.hyperlink), start: z.number(), end: z.number(), data, }) const LabelSpanSchema = z.object({ type: z.literal("label"), start: z.number(), end: z.number(), data: z.pipe( z.unknown(), z.transform((value) => (typeof value === "string" && value.length > 0 ? value : "")), ), }) type LabelSpan = z.infer const BasicSpanSchema = z.object({ type: z.union([ z.literal(RichTextNodeType.strong), z.literal(RichTextNodeType.em), z.literal("list-item"), // legacy case ]), start: z.number(), end: z.number(), }) type BasicSpan = z.infer export type Span = | HyperlinkSpan | LabelSpan | BasicSpan export type RichTextLegacySpan = Span // oxlint-disable-next-line explicit-module-boundary-types export const getSpanSchema = < TLinkSchema extends typeof FilledLinkLegacyValueSchema | typeof FilledLinkContentValueSchema, >( linkSchema: TLinkSchema, ) => z.discriminatedUnion("type", [ getHyperlinkSpanSchema(linkSchema), LabelSpanSchema, BasicSpanSchema, ]) // Text blocks const TextBlockTypeSchema = z.union([ z.literal(RichTextNodeType.paragraph), z.literal(RichTextNodeType.heading1), z.literal(RichTextNodeType.heading2), z.literal(RichTextNodeType.heading3), z.literal(RichTextNodeType.heading4), z.literal(RichTextNodeType.heading5), z.literal(RichTextNodeType.heading6), z.literal(RichTextNodeType.preformatted), z.literal(RichTextNodeType.oListItem), z.literal(RichTextNodeType.listItem), ]) // oxlint-disable-next-line explicit-module-boundary-types export const getTextBlockSchema = < TLinkSchema extends typeof FilledLinkLegacyValueSchema | typeof FilledLinkContentValueSchema, >( linkSchema: TLinkSchema, ) => z.object({ type: TextBlockTypeSchema, content: z.object({ text: z.string(), spans: z.optional( z.pipe( z.array(z.unknown()), z.transform((spans) => { const spanSchema = getSpanSchema(linkSchema) // Filter out invalid spans and order them by start position return spans .flatMap((maybeSpan) => { const parsed = spanSchema.safeParse(maybeSpan) return parsed.success ? [parsed.data] : [] }) .sort((a, b) => a.start - b.start) }), ), ), }), label: z.optional(z.string()), direction: z.optional(z.string()), }) const TextBlockLegacySchema = getTextBlockSchema(FilledLinkLegacyValueSchema) type TextBlockLegacy = z.infer // Image block // oxlint-disable-next-line explicit-module-boundary-types export const getImageBlockSchema = < TData extends typeof ImageLegacyViewSchema | typeof ImageContentViewSchema, TLinkTo extends typeof FilledLinkLegacyValueSchema | typeof FilledLinkContentValueSchema, >( data: TData, linkTo: TLinkTo, ) => z.object({ type: z.literal(RichTextNodeType.image), data: z.extend(data, { linkTo: z.nullish(linkTo) }), label: z.nullish(z.string()), direction: z.nullish(z.string()), }) const ImageBlockLegacySchema = getImageBlockSchema( ImageLegacyViewSchema, FilledLinkLegacyValueSchema, ) type ImageBlockLegacy = z.infer // Embed block // oxlint-disable-next-line explicit-module-boundary-types export const getEmbedBlockSchema = < TData extends typeof EmbedLegacyLooseSchema | typeof EmbedContentSchema, >( data: TData, ) => z.object({ type: z.literal(RichTextNodeType.embed), data, label: z.nullish(z.string()), direction: z.nullish(z.string()), }) const EmbedBlockLegacySchema = getEmbedBlockSchema(EmbedLegacyLooseSchema) type EmbedBlockLegacy = z.infer // Blocks export const RichTextLegacyBlockSchema: z.ZodMiniType = z.discriminatedUnion( "type", [TextBlockLegacySchema, ImageBlockLegacySchema, EmbedBlockLegacySchema], ) export type RichTextLegacyBlock = TextBlockLegacy | ImageBlockLegacy | EmbedBlockLegacy export const RichTextLegacySchema = z.array(RichTextLegacyBlockSchema) export type RichTextLegacy = z.infer