import { serializeEmbed, serializeHyperlink, serializeImage, serializePreFormatted, serializeSpan, serializeStandardTag, } from "../lib/serializerHelpers" import type { RichTextFunctionSerializer, RichTextMapSerializer, RichTextMapSerializerFunction, } from "../richtext" import { composeSerializers, serialize, wrapMapSerializer } from "../richtext" import type { RichTextField } from "../types/value/richText" import type { LinkResolverFunction } from "./asLink" /** * Serializes a node from a rich text field with a function to HTML. * * Unlike a typical `@prismicio/client/richtext` function serializer, this serializer converts the * `children` argument to a single string rather than an array of strings. * * @see Learn how to style rich text and customize rendering: {@link https://prismic.io/docs/fields/rich-text} */ export type HTMLRichTextFunctionSerializer = ( type: Parameters>[0], node: Parameters>[1], text: Parameters>[2], children: Parameters>[3][number], key: Parameters>[4], ) => string | null | undefined /** * Serializes a node from a rich text field with a map to HTML. * * Unlike a typical `@prismicio/client/richtext` map serializer, this serializer converts the * `children` property to a single string rather than an array of strings and accepts shorthand * declarations. * * @see Learn how to style rich text and customize rendering: {@link https://prismic.io/docs/fields/rich-text} */ export type HTMLRichTextMapSerializer = { [P in keyof RichTextMapSerializer]: P extends RichTextMapSerializer["span"] ? HTMLStrictRichTextMapSerializer[P] : HTMLStrictRichTextMapSerializer[P] | HTMLRichTextMapSerializerShorthand } /** * Serializes a node from a rich text field with a map to HTML. * * Unlike a typical `@prismicio/client/richtext` map serializer, this serializer converts the * `children` property to a single string rather than an array of strings but doesn't accept * shorthand declarations. * * @see Learn how to style rich text and customize rendering: {@link https://prismic.io/docs/fields/rich-text} */ export type HTMLStrictRichTextMapSerializer = { [P in keyof RichTextMapSerializer]: (payload: { type: Parameters>[0]["type"] node: Parameters>[0]["node"] text: Parameters>[0]["text"] children: Parameters>[0]["children"][number] key: Parameters>[0]["key"] }) => string | null | undefined } /** * A {@link RichTextMapSerializerFunction} type specifically for {@link HTMLRichTextMapSerializer}. * * @typeParam BlockName - The serializer's rich text block type. */ type HTMLRichTextMapSerializerFunction> = RichTextMapSerializerFunction< string, ExtractNodeGeneric[BlockType]>, ExtractTextTypeGeneric[BlockType]> > /** * Returns the `Node` generic from {@link RichTextMapSerializerFunction}. * * @typeParam T - The `RichTextMapSerializerFunction` containing the needed * `Node` generic. */ type ExtractNodeGeneric = T extends RichTextMapSerializerFunction< // oxlint-disable-next-line no-explicit-any any, infer U, // oxlint-disable-next-line no-explicit-any any > ? U : never /** * Returns the `TextType` generic from {@link RichTextMapSerializerFunction}. * * @typeParam T - The `RichTextMapSerializerFunction` containing the needed * `TextType` generic. */ type ExtractTextTypeGeneric = T extends RichTextMapSerializerFunction< // oxlint-disable-next-line no-explicit-any any, // oxlint-disable-next-line no-explicit-any any, infer U > ? U : never /** A shorthand definition for {@link HTMLRichTextMapSerializer} element types. */ export type HTMLRichTextMapSerializerShorthand = { /** Classes to apply to the element type. */ class?: string /** Other attributes to apply to the element type. */ [Attribute: string]: string | boolean | null | undefined } /** * Serializes a node from a rich text field with a map or a function to HTML. * * @see {@link HTMLRichTextMapSerializer} and {@link HTMLRichTextFunctionSerializer} * @see Learn how to style rich text and customize rendering: {@link https://prismic.io/docs/fields/rich-text} */ export type HTMLRichTextSerializer = HTMLRichTextMapSerializer | HTMLRichTextFunctionSerializer /** * Creates a HTML rich text serializer with a given link resolver and provide sensible and safe * defaults for every node type * * @internal */ const createHTMLRichTextSerializer = ( linkResolver: LinkResolverFunction | undefined | null, serializer?: HTMLRichTextMapSerializer | null, ): RichTextFunctionSerializer => { const useSerializerOrDefault = >( nodeSerializerOrShorthand: HTMLRichTextMapSerializer[BlockType], defaultWithShorthand: NonNullable, ): NonNullable => { if (typeof nodeSerializerOrShorthand === "function") { return ((payload) => { return ( (nodeSerializerOrShorthand as HTMLStrictRichTextMapSerializer[BlockType])?.(payload) || defaultWithShorthand(payload) ) }) as NonNullable } return defaultWithShorthand } const mapSerializer: Required = { heading1: useSerializerOrDefault<"heading1">( serializer?.heading1, serializeStandardTag<"heading1">("h1", serializer?.heading1), ), heading2: useSerializerOrDefault<"heading2">( serializer?.heading2, serializeStandardTag<"heading2">("h2", serializer?.heading2), ), heading3: useSerializerOrDefault<"heading3">( serializer?.heading3, serializeStandardTag<"heading3">("h3", serializer?.heading3), ), heading4: useSerializerOrDefault<"heading4">( serializer?.heading4, serializeStandardTag<"heading4">("h4", serializer?.heading4), ), heading5: useSerializerOrDefault<"heading5">( serializer?.heading5, serializeStandardTag<"heading5">("h5", serializer?.heading5), ), heading6: useSerializerOrDefault<"heading6">( serializer?.heading6, serializeStandardTag<"heading6">("h6", serializer?.heading6), ), paragraph: useSerializerOrDefault<"paragraph">( serializer?.paragraph, serializeStandardTag<"paragraph">("p", serializer?.paragraph), ), preformatted: useSerializerOrDefault<"preformatted">( serializer?.preformatted, serializePreFormatted(serializer?.preformatted), ), strong: useSerializerOrDefault<"strong">( serializer?.strong, serializeStandardTag<"strong">("strong", serializer?.strong), ), em: useSerializerOrDefault<"em">( serializer?.em, serializeStandardTag<"em">("em", serializer?.em), ), listItem: useSerializerOrDefault<"listItem">( serializer?.listItem, serializeStandardTag<"listItem">("li", serializer?.listItem), ), oListItem: useSerializerOrDefault<"oListItem">( serializer?.oListItem, serializeStandardTag<"oListItem">("li", serializer?.oListItem), ), list: useSerializerOrDefault<"list">( serializer?.list, serializeStandardTag<"list">("ul", serializer?.list), ), oList: useSerializerOrDefault<"oList">( serializer?.oList, serializeStandardTag<"oList">("ol", serializer?.oList), ), image: useSerializerOrDefault<"image">( serializer?.image, serializeImage(linkResolver, serializer?.image), ), embed: useSerializerOrDefault<"embed">(serializer?.embed, serializeEmbed(serializer?.embed)), hyperlink: useSerializerOrDefault<"hyperlink">( serializer?.hyperlink, serializeHyperlink(linkResolver, serializer?.hyperlink), ), label: useSerializerOrDefault<"label">( serializer?.label, serializeStandardTag<"label">("span", serializer?.label), ), span: useSerializerOrDefault<"span">(serializer?.span, serializeSpan()), } return wrapMapSerializerWithStringChildren(mapSerializer) } /** * Wraps a map serializer into a regular function serializer. The given map serializer should accept * children as a string, not as an array of strings like `@prismicio/client/richtext`'s * `wrapMapSerializer`. * * @param mapSerializer - Map serializer to wrap * @returns A regular function serializer */ const wrapMapSerializerWithStringChildren = ( mapSerializer: HTMLStrictRichTextMapSerializer, ): RichTextFunctionSerializer => { const modifiedMapSerializer = {} as RichTextMapSerializer for (const tag in mapSerializer) { const tagSerializer = mapSerializer[tag as keyof typeof mapSerializer] if (tagSerializer) { modifiedMapSerializer[tag as keyof typeof mapSerializer] = (payload) => { return tagSerializer({ ...payload, // @ts-expect-error - merging blockSerializer types causes TS to bail to a never type children: payload.children.join(""), }) } } } return wrapMapSerializer(modifiedMapSerializer) } /** Configuration that determines the output of `asHTML()`. */ type AsHTMLConfig = { /** * An optional link resolver function to resolve links. Without it, you're expected to use the * `routes` option from the API. */ linkResolver?: LinkResolverFunction | null /** An optional rich text serializer. Unhandled cases will fall back to the default serializer. */ serializer?: HTMLRichTextSerializer | null } // TODO: Remove when we remove support for deprecated tuple-style configuration. /** @deprecated Use object-style configuration instead. */ type AsHTMLDeprecatedTupleConfig = [ linkResolver?: LinkResolverFunction | null, serializer?: HTMLRichTextSerializer | null, ] /** The return type of `asHTML()`. */ type AsHTMLReturnType = Field extends RichTextField ? string : null // TODO: Remove overload when we remove support for deprecated tuple-style configuration. export const asHTML: { /** * Converts a rich text field to an HTML string. * * @example * ;```ts * const html = asHTML(document.data.content) * // => "

Hello world

" * ``` * * @param richTextField - A rich text field from Prismic. * @param config - Configuration that determines the output of `asHTML()`. * @returns HTML equivalent of the rich text field, or `null` if the field is * empty. * @see Learn how to style rich text and customize rendering: {@link https://prismic.io/docs/fields/rich-text} */ ( richTextField: Field, config?: AsHTMLConfig, ): AsHTMLReturnType /** * Converts a rich text field to an HTML string. * * @deprecated Use object-style configuration instead. * @param richTextField - A rich text field from Prismic. * @param linkResolver - An optional link resolver function to resolve links. Without it, you're * expected to use the `routes` option from the API. * @param serializer - An optional rich text serializer. Unhandled cases will fall back to the * default serializer. * @returns HTML equivalent of the rich text field, or `null` if the field is * empty. * @see Learn how to style rich text and customize rendering: {@link https://prismic.io/docs/fields/rich-text} */ ( richTextField: Field, ...config: AsHTMLDeprecatedTupleConfig ): AsHTMLReturnType } = ( richTextField: Field, // TODO: Rename to `config` when we remove support for deprecated tuple-style configuration. ...configObjectOrTuple: [config?: AsHTMLConfig] | AsHTMLDeprecatedTupleConfig ): AsHTMLReturnType => { if (richTextField) { // TODO: Remove when we remove support for deprecated tuple-style configuration. const [configObjectOrLinkResolver, maybeSerializer] = configObjectOrTuple let config: AsHTMLConfig if (typeof configObjectOrLinkResolver === "function" || configObjectOrLinkResolver == null) { config = { linkResolver: configObjectOrLinkResolver, serializer: maybeSerializer, } } else { config = { ...configObjectOrLinkResolver } } let serializer: RichTextFunctionSerializer if (config.serializer) { if (typeof config.serializer === "function") { serializer = composeSerializers( (type, node, text, children, key) => // TypeScript doesn't narrow the type correctly here since it is now in a callback function, so we have to cast it here. (config.serializer as HTMLRichTextFunctionSerializer)( type, node, text, children.join(""), key, ), createHTMLRichTextSerializer(config.linkResolver), ) } else { serializer = createHTMLRichTextSerializer(config.linkResolver, config.serializer) } } else { serializer = createHTMLRichTextSerializer(config.linkResolver) } return serialize(richTextField, serializer).join("") as AsHTMLReturnType } else { return null as AsHTMLReturnType } }