diff --git a/src/helpers/asHTML.ts b/src/helpers/asHTML.ts index 83d7a599..9883fb8a 100644 --- a/src/helpers/asHTML.ts +++ b/src/helpers/asHTML.ts @@ -122,10 +122,7 @@ export type HTMLRichTextSerializer = * @internal */ const createDefaultHTMLRichTextSerializer = ( - linkResolver: - | LinkResolverFunction - | undefined - | null, + linkResolver: LinkResolverFunction | undefined | null, ): RichTextFunctionSerializer => { return (_type, node, text, children, _key) => { switch (node.type) { @@ -203,41 +200,118 @@ const wrapMapSerializerWithStringChildren = ( 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 + */ + htmlRichTextSerializer?: HTMLRichTextSerializer | null; +}; + +// TODO: Remove when we remove support for deprecated tuple-style configuration. +/** + * @deprecated Use object-style configuration instead. + */ +type AsHTMLDeprecatedTupleConfig = [ + linkResolver?: LinkResolverFunction | null, + htmlRichTextSerializer?: HTMLRichTextSerializer | null, +]; + /** * The return type of `asHTML()`. */ type AsHTMLReturnType = Field extends RichTextField ? string : null; -/** - * Serializes a Rich Text or Title field to an HTML string - * - * @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 htmlRichTextSerializer - 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} - */ -export const asHTML = ( +// 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} + */ + ( + richTextField: Field, + config?: AsHTMLConfig, + ): AsHTMLReturnType; + + /** + * Serializes a Rich Text or Title field to an HTML string. + * + * @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 htmlRichTextSerializer - 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} + * + * @deprecated Use object-style configuration instead. + * + * ```ts + * asHTML(field); + * asHTML(field, { linkResolver }); + * asHTML(field, { htmlRichTextSerializer }); + * asHTML(field, { linkResolver, htmlRichTextSerializer }); + * ``` + */ + ( + richTextField: Field, + ...config: AsHTMLDeprecatedTupleConfig + ): AsHTMLReturnType; +} = ( richTextField: Field, - linkResolver?: LinkResolverFunction | null, - htmlRichTextSerializer?: HTMLRichTextSerializer | null, + // 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, maybeHTMLRichTextSerializer] = + configObjectOrTuple; + let config: AsHTMLConfig; + if ( + typeof configObjectOrLinkResolver === "function" || + configObjectOrLinkResolver == null + ) { + config = { + linkResolver: configObjectOrLinkResolver, + htmlRichTextSerializer: maybeHTMLRichTextSerializer, + }; + } else { + config = { ...configObjectOrLinkResolver }; + } + let serializer: RichTextFunctionSerializer; - if (htmlRichTextSerializer) { + if (config.htmlRichTextSerializer) { serializer = composeSerializers( - typeof htmlRichTextSerializer === "object" - ? wrapMapSerializerWithStringChildren(htmlRichTextSerializer) + typeof config.htmlRichTextSerializer === "object" + ? wrapMapSerializerWithStringChildren(config.htmlRichTextSerializer) : (type, node, text, children, key) => - htmlRichTextSerializer(type, node, text, children.join(""), key), - createDefaultHTMLRichTextSerializer(linkResolver), + // TypeScript doesn't narrow the type correctly here since it is now in a callback function, so we have to cast it here. + (config.htmlRichTextSerializer as HTMLRichTextFunctionSerializer)( + type, + node, + text, + children.join(""), + key, + ), + createDefaultHTMLRichTextSerializer(config.linkResolver), ); } else { - serializer = createDefaultHTMLRichTextSerializer(linkResolver); + serializer = createDefaultHTMLRichTextSerializer(config.linkResolver); } return serialize(richTextField, serializer).join( diff --git a/src/helpers/asImagePixelDensitySrcSet.ts b/src/helpers/asImagePixelDensitySrcSet.ts index bcd6891c..eddb1438 100644 --- a/src/helpers/asImagePixelDensitySrcSet.ts +++ b/src/helpers/asImagePixelDensitySrcSet.ts @@ -65,7 +65,7 @@ type AsImagePixelDensitySrcSetReturnType< * * @param field - Image field (or one of its responsive views) from which to get * an image URL. - * @param params - An object of Imgix URL API parameters. The `pixelDensities` + * @param config - An object of Imgix URL API parameters. The `pixelDensities` * parameter defines the resulting `srcset` widths. * * @returns A `srcset` attribute value for the Image field with Imgix URL @@ -76,12 +76,12 @@ export const asImagePixelDensitySrcSet = < Field extends ImageFieldImage | null | undefined, >( field: Field, - params: AsImagePixelDensitySrcSetConfig = {}, + config: AsImagePixelDensitySrcSetConfig = {}, ): AsImagePixelDensitySrcSetReturnType => { if (field && isImageThumbnailFilled(field)) { // We are using destructuring to omit `pixelDensities` from the // object we will pass to `buildURL()`. - const { pixelDensities = DEFAULT_PIXEL_DENSITIES, ...imgixParams } = params; + const { pixelDensities = DEFAULT_PIXEL_DENSITIES, ...imgixParams } = config; return { src: buildURL(field.url, imgixParams), diff --git a/src/helpers/asImageSrc.ts b/src/helpers/asImageSrc.ts index d21e84cb..cf94aa6f 100644 --- a/src/helpers/asImageSrc.ts +++ b/src/helpers/asImageSrc.ts @@ -23,7 +23,7 @@ type AsImageSrcReturnType = * * @param field - Image field (or one of its responsive views) from which to get * an image URL. - * @param params - An object of Imgix URL API parameters to transform the image. + * @param config - An object of Imgix URL API parameters to transform the image. * * @returns The Image field's image URL with transformations applied (if given). * If the Image field is empty, `null` is returned. @@ -31,10 +31,10 @@ type AsImageSrcReturnType = */ export const asImageSrc = ( field: Field, - params: ImgixURLParams = {}, + config: ImgixURLParams = {}, ): AsImageSrcReturnType => { if (field && isImageThumbnailFilled(field)) { - return buildURL(field.url, params) as AsImageSrcReturnType; + return buildURL(field.url, config) as AsImageSrcReturnType; } else { return null as AsImageSrcReturnType; } diff --git a/src/helpers/asImageWidthSrcSet.ts b/src/helpers/asImageWidthSrcSet.ts index 9c49d694..def1b73e 100644 --- a/src/helpers/asImageWidthSrcSet.ts +++ b/src/helpers/asImageWidthSrcSet.ts @@ -68,7 +68,7 @@ type AsImageWidthSrcSetConfig = Omit & { * * @param field - Image field (or one of its responsive views) from which to get * an image URL. - * @param params - An object of Imgix URL API parameters. The `widths` parameter + * @param config - An object of Imgix URL API parameters. The `widths` parameter * defines the resulting `srcset` widths. Pass `"thumbnails"` to automatically * use the field's responsive views. * @@ -80,7 +80,7 @@ export const asImageWidthSrcSet = < Field extends ImageFieldImage | null | undefined, >( field: Field, - params: AsImageWidthSrcSetConfig = {}, + config: AsImageWidthSrcSetConfig = {}, ): AsImageWidthSrcSetReturnType => { if (field && isFilled.imageThumbnail(field)) { // We are using destructuring to omit `widths` from the object @@ -89,7 +89,7 @@ export const asImageWidthSrcSet = < widths = DEFAULT_WIDTHS, // eslint-disable-next-line prefer-const ...imgixParams - } = params; + } = config; const { url, dimensions, diff --git a/src/helpers/asLink.ts b/src/helpers/asLink.ts index 75c096d7..311c61c7 100644 --- a/src/helpers/asLink.ts +++ b/src/helpers/asLink.ts @@ -19,6 +19,28 @@ export type LinkResolverFunction = ( linkToDocumentField: FilledContentRelationshipField, ) => ReturnType; +/** + * Configuration that determines the output of `asLink()`. + */ +type AsLinkConfig = + { + /** + * An optional Link Resolver function. Without it, you are + * expected to use the `routes` options from the API. + */ + linkResolver?: LinkResolverFunction | null; + }; + +// TODO: Remove when we remove support for deprecated tuple-style configuration. +/** + * @deprecated Use object-style configuration instead. + */ +type AsLinkDeprecatedTupleConfig< + LinkResolverFunctionReturnType = string | null | undefined, +> = [ + linkResolver?: LinkResolverFunction | null, +]; + /** * The return type of `asLink()`. */ @@ -37,21 +59,66 @@ export type AsLinkReturnType< ? LinkResolverFunctionReturnType | string | null : null; -/** - * Resolves any type of Link field or Prismic document to a URL. - * - * @typeParam LinkResolverFunctionReturnType - Link Resolver function return - * type - * @typeParam Field - Link field or Prismic document to resolve to a URL - * @param linkFieldOrDocument - Any kind of Link field or a document to resolve - * @param linkResolver - An optional Link Resolver function. Without it, you are - * expected to use the `routes` options from the API - * - * @returns Resolved URL or, if the provided Link field or document is empty, `null` - * @see Prismic Link Resolver documentation: {@link https://prismic.io/docs/route-resolver#link-resolver} - * @see Prismic API `routes` options documentation: {@link https://prismic.io/docs/route-resolver} - */ -export const asLink = < +// TODO: Remove overload when we remove support for deprecated tuple-style configuration. +export const asLink: { + /** + * Resolves any type of Link field or Prismic document to a URL. + * + * @typeParam LinkResolverFunctionReturnType - Link Resolver function return + * type + * @typeParam Field - Link field or Prismic document to resolve to a URL + * @param linkFieldOrDocument - Any kind of Link field or a document to resolve + * @param config - Configuration that determines the output of `asLink()` + * + * @returns Resolved URL or, if the provided Link field or document is empty, `null` + * @see Prismic Link Resolver documentation: {@link https://prismic.io/docs/route-resolver#link-resolver} + * @see Prismic API `routes` options documentation: {@link https://prismic.io/docs/route-resolver} + */ + < + LinkResolverFunctionReturnType = string | null | undefined, + Field extends LinkField | PrismicDocument | null | undefined = + | LinkField + | PrismicDocument + | null + | undefined, + >( + linkFieldOrDocument: Field, + config?: AsLinkConfig, + ): AsLinkReturnType; + + /** + * Resolves any type of Link field or Prismic document to a URL. + * + * @typeParam LinkResolverFunctionReturnType - Link Resolver function return + * type + * @typeParam Field - Link field or Prismic document to resolve to a URL + * @param linkFieldOrDocument - Any kind of Link field or a document to resolve + * @param linkResolver - An optional Link Resolver function. Without it, you are + * expected to use the `routes` options from the API + * + * @returns Resolved URL or, if the provided Link field or document is empty, `null` + * @see Prismic Link Resolver documentation: {@link https://prismic.io/docs/route-resolver#link-resolver} + * @see Prismic API `routes` options documentation: {@link https://prismic.io/docs/route-resolver} + * + * @deprecated Use object-style configuration instead. + * + * ```ts + * asLink(field); + * asLink(field, { linkResolver }); + * ``` + */ + < + LinkResolverFunctionReturnType = string | null | undefined, + Field extends LinkField | PrismicDocument | null | undefined = + | LinkField + | PrismicDocument + | null + | undefined, + >( + linkFieldOrDocument: Field, + ...config: AsLinkDeprecatedTupleConfig + ): AsLinkReturnType; +} = < LinkResolverFunctionReturnType = string | null | undefined, Field extends LinkField | PrismicDocument | null | undefined = | LinkField @@ -60,7 +127,10 @@ export const asLink = < | undefined, >( linkFieldOrDocument: Field, - linkResolver?: LinkResolverFunction | null, + // TODO: Rename to `config` when we remove support for deprecated tuple-style configuration. + ...configObjectOrTuple: + | [config?: AsLinkConfig] + | AsLinkDeprecatedTupleConfig ): AsLinkReturnType => { if (!linkFieldOrDocument) { return null as AsLinkReturnType; @@ -78,6 +148,20 @@ export const asLink = < : documentToLinkField(linkFieldOrDocument) ) as LinkField; + // TODO: Remove when we remove support for deprecated tuple-style configuration. + const [configObjectOrLinkResolver] = configObjectOrTuple; + let config: AsLinkConfig; + if ( + typeof configObjectOrLinkResolver === "function" || + configObjectOrLinkResolver == null + ) { + config = { + linkResolver: configObjectOrLinkResolver, + }; + } else { + config = { ...configObjectOrLinkResolver }; + } + switch (linkField.link_type) { case LinkType.Media: case LinkType.Web: @@ -87,9 +171,9 @@ export const asLink = < >; case LinkType.Document: { - if ("id" in linkField && linkResolver) { + if ("id" in linkField && config.linkResolver) { // When using Link Resolver... - const resolvedURL = linkResolver(linkField); + const resolvedURL = config.linkResolver(linkField); if (resolvedURL != null) { return resolvedURL as AsLinkReturnType< diff --git a/src/helpers/asText.ts b/src/helpers/asText.ts index 5f2fdac5..bb7a87af 100644 --- a/src/helpers/asText.ts +++ b/src/helpers/asText.ts @@ -2,27 +2,86 @@ import { asText as baseAsText } from "@prismicio/richtext"; import { RichTextField } from "../types/value/richText"; +/** + * Configuration that determines the output of `asText()`. + */ +type AsTextConfig = { + /** + * Separator used to join each element. + * + * @defaultValue ` ` (a space) + */ + separator?: string; +}; + +// TODO: Remove when we remove support for deprecated tuple-style configuration. +/** + * @deprecated Use object-style configuration instead. + */ +type AsTextDeprecatedTupleConfig = [separator?: string]; + /** * The return type of `asText()`. */ type AsTextReturnType = Field extends RichTextField ? string : null; -/** - * Serializes a Rich Text or Title field to a plain text string - * - * @param richTextField - A Rich Text or Title field from Prismic - * @param separator - Separator used to join each element, defaults to a space - * - * @returns Plain text 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} - */ -export const asText = ( +export const asText: { + /** + * Serializes a Rich Text or Title field to a plain text string. + * + * @param richTextField - A Rich Text or Title field from Prismic + * @param config - Configuration that determines the output of `asText()` + * + * @returns Plain text 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} + */ + ( + richTextField: Field, + config?: AsTextConfig, + ): AsTextReturnType; + + /** + * Serializes a Rich Text or Title field to a plain text string. + * + * @param richTextField - A Rich Text or Title field from Prismic + * @param separator - Separator used to join each element, defaults to a space + * + * @returns Plain text 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} + * + * @deprecated Use object-style configuration instead. + * + * ```ts + * asText(field); + * asText(field, { separator }); + * ``` + */ + ( + richTextField: Field, + ...config: AsTextDeprecatedTupleConfig + ): AsTextReturnType; +} = ( richTextField: Field, - separator?: string, + // TODO: Rename to `config` when we remove support for deprecated tuple-style configuration. + ...configObjectOrTuple: [config?: AsTextConfig] | AsTextDeprecatedTupleConfig ): AsTextReturnType => { if (richTextField) { - return baseAsText(richTextField, separator) as AsTextReturnType; + // TODO: Remove when we remove support for deprecated tuple-style configuration. + const [configObjectOrSeparator] = configObjectOrTuple; + let config: AsTextConfig; + if (typeof configObjectOrSeparator === "string") { + config = { + separator: configObjectOrSeparator, + }; + } else { + config = { ...configObjectOrSeparator }; + } + + return baseAsText( + richTextField, + config.separator, + ) as AsTextReturnType; } else { return null as AsTextReturnType; } diff --git a/test/helpers-asHTML.test.ts b/test/helpers-asHTML.test.ts index c958ee2c..0979e4cb 100644 --- a/test/helpers-asHTML.test.ts +++ b/test/helpers-asHTML.test.ts @@ -8,23 +8,54 @@ import { richTextFixture } from "./__fixtures__/richText"; import { RichTextField, asHTML } from "../src"; it("serializes with default serializer", () => { - expect(asHTML(richTextFixture.en, linkResolver)).toMatchSnapshot(); + expect(asHTML(richTextFixture.en, { linkResolver })).toMatchSnapshot(); + + // TODO: Remove when we remove support for deprecated tuple-style configuration. + expect(asHTML(richTextFixture.en, linkResolver)).toBe( + asHTML(richTextFixture.en, { linkResolver }), + ); }); it("serializes with a custom function serializer", () => { expect( - asHTML(richTextFixture.en, linkResolver, htmlRichTextFunctionSerializer), + asHTML(richTextFixture.en, { + linkResolver, + htmlRichTextSerializer: htmlRichTextFunctionSerializer, + }), ).toMatchSnapshot(); + + // TODO: Remove when we remove support for deprecated tuple-style configuration. + expect( + asHTML(richTextFixture.en, linkResolver, htmlRichTextFunctionSerializer), + ).toBe( + asHTML(richTextFixture.en, { + linkResolver, + htmlRichTextSerializer: htmlRichTextFunctionSerializer, + }), + ); }); it("serializes with a custom map serializer", () => { expect( - asHTML(richTextFixture.en, linkResolver, htmlRichTextMapSerializer), + asHTML(richTextFixture.en, { + linkResolver, + htmlRichTextSerializer: htmlRichTextMapSerializer, + }), ).toMatchSnapshot(); + + // TODO: Remove when we remove support for deprecated tuple-style configuration. + expect( + asHTML(richTextFixture.en, linkResolver, htmlRichTextMapSerializer), + ).toBe( + asHTML(richTextFixture.en, { + linkResolver, + htmlRichTextSerializer: htmlRichTextMapSerializer, + }), + ); }); it("escapes external links to prevent XSS", () => { - expect(asHTML(richTextFixture.xss, linkResolver)).toMatchSnapshot(); + expect(asHTML(richTextFixture.xss, { linkResolver })).toMatchSnapshot(); }); it("omits target attribute on links without a target value", () => { @@ -46,7 +77,7 @@ it("omits target attribute on links without a target value", () => { }, ]; - expect(asHTML(field, linkResolver)).toMatchInlineSnapshot( + expect(asHTML(field, { linkResolver })).toMatchInlineSnapshot( '"

link

"', ); }); @@ -71,7 +102,7 @@ it("includes target attribute on links with a target value", () => { }, ]; - expect(asHTML(field, linkResolver)).toMatchInlineSnapshot( + expect(asHTML(field, { linkResolver })).toMatchInlineSnapshot( '"

link

"', ); }); diff --git a/test/helpers-asLink.test.ts b/test/helpers-asLink.test.ts index b7f779ee..bd59e2ca 100644 --- a/test/helpers-asLink.test.ts +++ b/test/helpers-asLink.test.ts @@ -51,15 +51,20 @@ it("resolves a link to document field without Route Resolver", () => { "returns null if both Link Resolver and Route Resolver are not used", ).toBeNull(); expect( - asLink(field, linkResolver), + asLink(field, { linkResolver }), "uses Link Resolver URL if Link Resolver returns a non-nullish value", ).toBe("/test"); + // TODO: Remove when we remove support for deprecated tuple-style configuration. + expect( + asLink(field, linkResolver), + "uses Link Resolver URL if Link Resolver returns a non-nullish value (deprecated tuple configuration)", + ).toBe("/test"); expect( - asLink(field, () => undefined), + asLink(field, { linkResolver: () => undefined }), "returns null if Link Resolver returns undefined", ).toBeNull(); expect( - asLink(field, () => null), + asLink(field, { linkResolver: () => null }), "returns null if Link Resolver returns null", ).toBeNull(); }); @@ -82,15 +87,15 @@ it("resolves a link to document field with Route Resolver", () => { "uses Route Resolver URL if Link Resolver is not given", ).toBe(field.url); expect( - asLink(field, () => "link-resolver-value"), + asLink(field, { linkResolver: () => "link-resolver-value" }), "uses Link Resolver URL if Link Resolver returns a non-nullish value", ).toBe("link-resolver-value"); expect( - asLink(field, () => undefined), + asLink(field, { linkResolver: () => undefined }), "uses Route Resolver URL if Link Resolver returns undefined", ).toBe(field.url); expect( - asLink(field, () => null), + asLink(field, { linkResolver: () => null }), "uses Route Resolver URL if Link Resolver returns null", ).toBe(field.url); }); @@ -110,7 +115,7 @@ it("resolves a link to web field", () => { url: "https://prismic.io", }; - expect(asLink(field, linkResolver)).toBe("https://prismic.io"); + expect(asLink(field, { linkResolver })).toBe("https://prismic.io"); }); it("resolves a link to media field", () => { @@ -124,7 +129,7 @@ it("resolves a link to media field", () => { width: "42", }; - expect(asLink(field, linkResolver)).toBe("https://prismic.io"); + expect(asLink(field, { linkResolver })).toBe("https://prismic.io"); }); it("resolves a document", () => { diff --git a/test/helpers-asText.test.ts b/test/helpers-asText.test.ts index eedf1c04..2b9844f5 100644 --- a/test/helpers-asText.test.ts +++ b/test/helpers-asText.test.ts @@ -14,3 +14,14 @@ it("returns null for nullish inputs", () => { expect(asText(null)).toBeNull(); expect(asText(undefined)).toBeNull(); }); + +it("supports separator configuration", () => { + expect(asText(richTextFixture.en, { separator: "__separator__" })).toBe( + prismicR.asText(richTextFixture.en, "__separator__"), + ); + + // TODO: Remove when we remove support for deprecated tuple-style configuration. + expect(asText(richTextFixture.en, "__separator__")).toBe( + prismicR.asText(richTextFixture.en, "__separator__"), + ); +});