File
Blob: src/shared/sites/island-schemas.ts
| 1 | import { z } from "zod/mini"; |
| 2 | import { MAX_IMAGE_BRIGHTNESS, MIN_IMAGE_BRIGHTNESS } from "@/shared/editor/schema/image-model"; |
| 3 | |
| 4 | const OutlineLevelSchema = z.union([ |
| 5 | z.literal(1), |
| 6 | z.literal(2), |
| 7 | z.literal(3), |
| 8 | z.literal(4), |
| 9 | z.literal(5), |
| 10 | z.literal(6), |
| 11 | ]); |
| 12 | |
| 13 | export const OutlineItemSchema = z.object({ |
| 14 | id: z.string(), |
| 15 | text: z.string(), |
| 16 | level: OutlineLevelSchema, |
| 17 | href: z.optional(z.string()), |
| 18 | }); |
| 19 | |
| 20 | export const SitesImagePropsSchema = z.object({ |
| 21 | bid: z.optional(z.nullable(z.string())), |
| 22 | src: z.string().check(z.minLength(1)), |
| 23 | alt: z.optional(z.nullable(z.string())), |
| 24 | title: z.optional(z.nullable(z.string())), |
| 25 | align: z._default(z.enum(["left", "center", "right"]), "left"), |
| 26 | width: z.optional(z.nullable(z.number().check(z.positive()))), |
| 27 | naturalWidth: z.optional(z.nullable(z.number().check(z.positive()))), |
| 28 | naturalHeight: z.optional(z.nullable(z.number().check(z.positive()))), |
| 29 | brightness: z.optional(z.number().check(z.minimum(MIN_IMAGE_BRIGHTNESS), z.maximum(MAX_IMAGE_BRIGHTNESS))), |
| 30 | }); |
| 31 | |
| 32 | export type SitesImageProps = z.infer<typeof SitesImagePropsSchema>; |
| 33 | |
| 34 | export const CopyCodeButtonPropsSchema = z.strictObject({}); |
| 35 | |
| 36 | export type CopyCodeButtonProps = z.infer<typeof CopyCodeButtonPropsSchema>; |
| 37 | |
| 38 | export const SiteOutlineControllerPropsSchema = z.object({ |
| 39 | items: z.array(OutlineItemSchema), |
| 40 | }); |
| 41 | |
| 42 | export type SiteOutlineControllerProps = z.infer<typeof SiteOutlineControllerPropsSchema>; |
| 43 | |
| 44 | export const ISLAND_PROP_SCHEMAS = { |
| 45 | "sites-image": SitesImagePropsSchema, |
| 46 | "copy-code": CopyCodeButtonPropsSchema, |
| 47 | "site-outline-controller": SiteOutlineControllerPropsSchema, |
| 48 | } as const; |
| 49 | |
| 50 | export type IslandName = keyof typeof ISLAND_PROP_SCHEMAS; |
| 51 | export type IslandPropsByName = { |
| 52 | [K in IslandName]: z.infer<(typeof ISLAND_PROP_SCHEMAS)[K]>; |
| 53 | }; |
| 54 | |
| 55 | export function isIslandName(name: string | undefined): name is IslandName { |
| 56 | return name === "sites-image" || name === "copy-code" || name === "site-outline-controller"; |
| 57 | } |
| 58 | |
| 59 | type SafeParseSchema<T> = { |
| 60 | safeParse(value: unknown): { success: true; data: T } | { success: false }; |
| 61 | }; |
| 62 | |
| 63 | function parseWithSchema<T>(schema: SafeParseSchema<T>, value: unknown): T | null { |
| 64 | const parsed = schema.safeParse(value); |
| 65 | return parsed.success ? parsed.data : null; |
| 66 | } |
| 67 | |
| 68 | export function readIslandProps(name: "sites-image", text: string): SitesImageProps | null; |
| 69 | export function readIslandProps(name: "copy-code", text: string): CopyCodeButtonProps | null; |
| 70 | export function readIslandProps(name: "site-outline-controller", text: string): SiteOutlineControllerProps | null; |
| 71 | export function readIslandProps(name: IslandName, text: string): IslandPropsByName[IslandName] | null { |
| 72 | let value: unknown; |
| 73 | try { |
| 74 | value = JSON.parse(text); |
| 75 | } catch { |
| 76 | return null; |
| 77 | } |
| 78 | |
| 79 | switch (name) { |
| 80 | case "sites-image": |
| 81 | return parseWithSchema(SitesImagePropsSchema, value); |
| 82 | case "copy-code": |
| 83 | return parseWithSchema(CopyCodeButtonPropsSchema, value); |
| 84 | case "site-outline-controller": |
| 85 | return parseWithSchema(SiteOutlineControllerPropsSchema, value); |
| 86 | } |
| 87 | } |