Skip to content

Commit

Permalink
refactor(helpers): standardize helpers interface (#288)
Browse files Browse the repository at this point in the history
* refactor(helpers): rename `params` to `config` on image helpers

* refactor(helpers): standardize helpers interface

* refactor(helpers): use arrow function overload for better DX

* fix(helpers): types

---------

Co-authored-by: lihbr <[email protected]>
  • Loading branch information
lihbr and lihbr authored May 3, 2023
1 parent 42013e5 commit 59f84ed
Show file tree
Hide file tree
Showing 9 changed files with 342 additions and 78 deletions.
124 changes: 99 additions & 25 deletions src/helpers/asHTML.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,7 @@ export type HTMLRichTextSerializer =
* @internal
*/
const createDefaultHTMLRichTextSerializer = (
linkResolver:
| LinkResolverFunction<string | null | undefined>
| undefined
| null,
linkResolver: LinkResolverFunction | undefined | null,
): RichTextFunctionSerializer<string> => {
return (_type, node, text, children, _key) => {
switch (node.type) {
Expand Down Expand Up @@ -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 | null | undefined> =
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 = <Field extends RichTextField | null | undefined>(
// 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.
*
* @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 });
* ```
*/
<Field extends RichTextField | null | undefined>(
richTextField: Field,
...config: AsHTMLDeprecatedTupleConfig
): AsHTMLReturnType<Field>;
} = <Field extends RichTextField | null | undefined>(
richTextField: Field,
linkResolver?: LinkResolverFunction<string | null | undefined> | null,
htmlRichTextSerializer?: HTMLRichTextSerializer | null,
// 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, maybeHTMLRichTextSerializer] =
configObjectOrTuple;
let config: AsHTMLConfig;
if (
typeof configObjectOrLinkResolver === "function" ||
configObjectOrLinkResolver == null
) {
config = {
linkResolver: configObjectOrLinkResolver,
htmlRichTextSerializer: maybeHTMLRichTextSerializer,
};
} else {
config = { ...configObjectOrLinkResolver };
}

let serializer: RichTextFunctionSerializer<string>;
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(
Expand Down
6 changes: 3 additions & 3 deletions src/helpers/asImagePixelDensitySrcSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -76,12 +76,12 @@ export const asImagePixelDensitySrcSet = <
Field extends ImageFieldImage | null | undefined,
>(
field: Field,
params: AsImagePixelDensitySrcSetConfig = {},
config: AsImagePixelDensitySrcSetConfig = {},
): AsImagePixelDensitySrcSetReturnType<Field> => {
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),
Expand Down
6 changes: 3 additions & 3 deletions src/helpers/asImageSrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,18 @@ type AsImageSrcReturnType<Field extends ImageFieldImage | null | undefined> =
*
* @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.
* @see Imgix URL parameters reference: https://docs.imgix.com/apis/rendering
*/
export const asImageSrc = <Field extends ImageFieldImage | null | undefined>(
field: Field,
params: ImgixURLParams = {},
config: ImgixURLParams = {},
): AsImageSrcReturnType<Field> => {
if (field && isImageThumbnailFilled(field)) {
return buildURL(field.url, params) as AsImageSrcReturnType<Field>;
return buildURL(field.url, config) as AsImageSrcReturnType<Field>;
} else {
return null as AsImageSrcReturnType<Field>;
}
Expand Down
6 changes: 3 additions & 3 deletions src/helpers/asImageWidthSrcSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ type AsImageWidthSrcSetConfig = Omit<BuildWidthSrcSetParams, "widths"> & {
*
* @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.
*
Expand All @@ -80,7 +80,7 @@ export const asImageWidthSrcSet = <
Field extends ImageFieldImage | null | undefined,
>(
field: Field,
params: AsImageWidthSrcSetConfig = {},
config: AsImageWidthSrcSetConfig = {},
): AsImageWidthSrcSetReturnType<Field> => {
if (field && isFilled.imageThumbnail(field)) {
// We are using destructuring to omit `widths` from the object
Expand All @@ -89,7 +89,7 @@ export const asImageWidthSrcSet = <
widths = DEFAULT_WIDTHS,
// eslint-disable-next-line prefer-const
...imgixParams
} = params;
} = config;
const {
url,
dimensions,
Expand Down
120 changes: 102 additions & 18 deletions src/helpers/asLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,28 @@ export type LinkResolverFunction<ReturnType = string | null | undefined> = (
linkToDocumentField: FilledContentRelationshipField,
) => ReturnType;

/**
* Configuration that determines the output of `asLink()`.
*/
type AsLinkConfig<LinkResolverFunctionReturnType = string | null | undefined> =
{
/**
* An optional Link Resolver function. Without it, you are
* expected to use the `routes` options from the API.
*/
linkResolver?: LinkResolverFunction<LinkResolverFunctionReturnType> | 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<LinkResolverFunctionReturnType> | null,
];

/**
* The return type of `asLink()`.
*/
Expand All @@ -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<LinkResolverFunctionReturnType>,
): AsLinkReturnType<LinkResolverFunctionReturnType, Field>;

/**
* 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<LinkResolverFunctionReturnType>
): AsLinkReturnType<LinkResolverFunctionReturnType, Field>;
} = <
LinkResolverFunctionReturnType = string | null | undefined,
Field extends LinkField | PrismicDocument | null | undefined =
| LinkField
Expand All @@ -60,7 +127,10 @@ export const asLink = <
| undefined,
>(
linkFieldOrDocument: Field,
linkResolver?: LinkResolverFunction<LinkResolverFunctionReturnType> | null,
// TODO: Rename to `config` when we remove support for deprecated tuple-style configuration.
...configObjectOrTuple:
| [config?: AsLinkConfig<LinkResolverFunctionReturnType>]
| AsLinkDeprecatedTupleConfig<LinkResolverFunctionReturnType>
): AsLinkReturnType<LinkResolverFunctionReturnType, Field> => {
if (!linkFieldOrDocument) {
return null as AsLinkReturnType<LinkResolverFunctionReturnType, Field>;
Expand All @@ -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<LinkResolverFunctionReturnType>;
if (
typeof configObjectOrLinkResolver === "function" ||
configObjectOrLinkResolver == null
) {
config = {
linkResolver: configObjectOrLinkResolver,
};
} else {
config = { ...configObjectOrLinkResolver };
}

switch (linkField.link_type) {
case LinkType.Media:
case LinkType.Web:
Expand All @@ -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<
Expand Down
Loading

0 comments on commit 59f84ed

Please sign in to comment.