@prismicio/client
Version:
The official JavaScript + TypeScript client library for Prismic
410 lines (381 loc) • 13.7 kB
text/typescript
import {
serializeEmbed,
serializeHyperlink,
serializeImage,
serializePreFormatted,
serializeSpan,
serializeStandardTag,
} from "../lib/serializerHelpers"
import type { RichTextField } from "../types/value/richText"
import type {
RichTextFunctionSerializer,
RichTextMapSerializer,
RichTextMapSerializerFunction,
} from "../richtext"
import { composeSerializers, serialize, wrapMapSerializer } from "../richtext"
import type { LinkResolverFunction } from "./asLink"
/**
* Serializes a node from a rich text or title 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 Templating rich text and title fields from Prismic {@link https://prismic.io/docs/template-content-vanilla-javascript#rich-text-and-title}
*/
export type HTMLRichTextFunctionSerializer = (
type: Parameters<RichTextFunctionSerializer<string>>[0],
node: Parameters<RichTextFunctionSerializer<string>>[1],
text: Parameters<RichTextFunctionSerializer<string>>[2],
children: Parameters<RichTextFunctionSerializer<string>>[3][number],
key: Parameters<RichTextFunctionSerializer<string>>[4],
) => string | null | undefined
/**
* Serializes a node from a rich text or title 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 Templating rich text and title fields from Prismic {@link https://prismic.io/docs/template-content-vanilla-javascript#rich-text-and-title}
*/
export type HTMLRichTextMapSerializer = {
[P in keyof RichTextMapSerializer<string>]: P extends RichTextMapSerializer<string>["span"]
? HTMLStrictRichTextMapSerializer[P]
: HTMLStrictRichTextMapSerializer[P] | HTMLRichTextMapSerializerShorthand
}
/**
* Serializes a node from a rich text or title 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 Templating rich text and title fields from Prismic {@link https://prismic.io/docs/template-content-vanilla-javascript#rich-text-and-title}
*/
export type HTMLStrictRichTextMapSerializer = {
[P in keyof RichTextMapSerializer<string>]: (payload: {
type: Parameters<HTMLRichTextMapSerializerFunction<P>>[0]["type"]
node: Parameters<HTMLRichTextMapSerializerFunction<P>>[0]["node"]
text: Parameters<HTMLRichTextMapSerializerFunction<P>>[0]["text"]
children: Parameters<
HTMLRichTextMapSerializerFunction<P>
>[0]["children"][number]
key: Parameters<HTMLRichTextMapSerializerFunction<P>>[0]["key"]
}) => string | null | undefined
}
/**
* A {@link RichTextMapSerializerFunction} type specifically for
* {@link HTMLRichTextMapSerializer}.
*
* @typeParam BlockName - The serializer's rich text block type.
*/
type HTMLRichTextMapSerializerFunction<
BlockType extends keyof RichTextMapSerializer<string>,
> = RichTextMapSerializerFunction<
string,
ExtractNodeGeneric<RichTextMapSerializer<string>[BlockType]>,
ExtractTextTypeGeneric<RichTextMapSerializer<string>[BlockType]>
>
/**
* Returns the `Node` generic from {@link RichTextMapSerializerFunction}.
*
* @typeParam T - The `RichTextMapSerializerFunction` containing the needed
* `Node` generic.
*/
type ExtractNodeGeneric<T> =
T extends RichTextMapSerializerFunction<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any,
infer U,
// eslint-disable-next-line @typescript-eslint/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> =
T extends RichTextMapSerializerFunction<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any,
// eslint-disable-next-line @typescript-eslint/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 or title field with a map or a function to
* HTML
*
* @see {@link HTMLRichTextMapSerializer} and {@link HTMLRichTextFunctionSerializer}
* @see Templating rich text and title fields from Prismic {@link https://prismic.io/docs/template-content-vanilla-javascript#rich-text-and-title}
*/
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<string> => {
const useSerializerOrDefault = <
BlockType extends keyof RichTextMapSerializer<string>,
>(
nodeSerializerOrShorthand: HTMLRichTextMapSerializer[BlockType],
defaultWithShorthand: NonNullable<
HTMLStrictRichTextMapSerializer[BlockType]
>,
): NonNullable<HTMLStrictRichTextMapSerializer[BlockType]> => {
if (typeof nodeSerializerOrShorthand === "function") {
return ((payload) => {
return (
(
nodeSerializerOrShorthand as HTMLStrictRichTextMapSerializer[BlockType]
)?.(payload) || defaultWithShorthand(payload)
)
}) as NonNullable<HTMLStrictRichTextMapSerializer[BlockType]>
}
return defaultWithShorthand
}
const mapSerializer: Required<HTMLStrictRichTextMapSerializer> = {
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<string> => {
const modifiedMapSerializer = {} as RichTextMapSerializer<string>
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` options from the API.
*/
linkResolver?: LinkResolverFunction | null
/**
* An optional rich text serializer, unhandled cases will fallback 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 | null | undefined> =
Field extends RichTextField ? string : null
// TODO: Remove overload when we remove support for deprecated tuple-style configuration.
export const asHTML: {
/**
* Serializes a rich text or title field to an HTML string.
*
* @param richTextField - A rich text or title field from Prismic
* @param config - Configuration that determines the output of `asHTML()`
*
* @returns HTML equivalent of the provided rich text or title field
*
* @see Templating rich text and title fields from Prismic {@link https://prismic.io/docs/template-content-vanilla-javascript#rich-text-and-title}
*/
<Field extends RichTextField | null | undefined>(
richTextField: Field,
config?: AsHTMLConfig,
): AsHTMLReturnType<Field>
/**
* Serializes a rich text or title field to an HTML string.
*
* @deprecated Use object-style configuration instead.
*
* @param richTextField - A rich text or title field from Prismic
* @param linkResolver - An optional link resolver function to resolve links,
* without it you're expected to use the `routes` options from the API
* @param serializer - An optional rich text serializer, unhandled cases will
* fallback to the default serializer
*
* @returns HTML equivalent of the provided rich text or title field
*
* @see Templating rich text and title fields from Prismic {@link https://prismic.io/docs/template-content-vanilla-javascript#rich-text-and-title}
*/
<Field extends RichTextField | null | undefined>(
richTextField: Field,
...config: AsHTMLDeprecatedTupleConfig
): AsHTMLReturnType<Field>
} = <Field extends RichTextField | null | undefined>(
richTextField: Field,
// TODO: Rename to `config` when we remove support for deprecated tuple-style configuration.
...configObjectOrTuple: [config?: AsHTMLConfig] | AsHTMLDeprecatedTupleConfig
): AsHTMLReturnType<Field> => {
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<string>
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<Field>
} else {
return null as AsHTMLReturnType<Field>
}
}