/** * @internal * @private */ export const F_AUTO = `f_auto` /** * Helper function to be used as the Array.prototype.filter callback to filter out `f_auto`. * @param bit - the bit to check * @returns true or false * @internal * @private */ const removeFAuto = (bit: string) => bit !== F_AUTO /** * A helper function that allows you to inject a string of Cloudinary transforms into an existing Cloudinary src URL. * * @param src - the Cloudinary URL to inject transforms into * @param transforms - the transforms to inject into the Cloudinary URL * @returns a valid Cloudinary URL with the transforms injected in the correct place * * @example * ### NOTE: the result is… * 1. a valid Cloudinary URL for use in ResponsiveImage’s src prop * 1. has `f_auto` added * 1. `e_sharpen` and `e_blur` transforms appears last in the URL * 1. if subsequent transforms are already present, new ones are added later in the chain * * ```js * const cloudinarySrc = `https://res.cloudinary.com/mabx-eu-uat/image/upload/v1/demo/osrxy6zh0rrpaqx012c7` * const transforms = `e_sharpen:50/c_crop,h_0.40,w_0.65` * const result = injectTransforms(cloudinarySrc, transforms) * * // result = `https://res.cloudinary.com/mabx-eu-uat/image/upload/f_auto/c_crop,h_0.40,w_0.65/e_sharpen:50/v1/demo/osrxy6zh0rrpaqx012c7` * ``` * @see {@link ResponsiveImage} */ export const injectTransforms = (src = ``, transforms = ``, insertBeforePattern: RegExp = insertionBitPattern) => { if (!src) return `` const srcUrl = new URL(src) const srcBits = srcUrl.pathname.split(`/`) const insertionBitIndex = srcBits.findIndex((bit) => afterEffectsPattern.test(bit) || insertBeforePattern.test(bit)) const transformBits = transforms .split(`/`) .filter(removeFAuto) .map((bit) => bit.split(`,`).filter(removeFAuto).join(`,`)) // sort items containing e_ last transformBits.sort(effectsLast) const afterEffectsBitIndex = transformBits.findIndex((bit) => afterEffectsPattern.test(bit)) const afterEffectsBit = afterEffectsBitIndex > -1 ? transformBits.splice(afterEffectsBitIndex, 1) : `` const earlyTransforms = transformBits.join(`/`) const lateTransforms = afterEffectsBit const uniqueBits = new Set([ // everything before insertionBit ...srcBits.slice(0, insertionBitIndex), // we always want `f_auto` F_AUTO, // cropping, resize, and similar transforms should happen before others earlyTransforms, // sharpening and blurring transforms should happen after sizing and cropping, etc. lateTransforms, // the rest of the URL which identifies the source image, including insertionBit ...srcBits.slice(insertionBitIndex), ]) // the order in which the transforms are listed is important… const finalBits = [...uniqueBits].filter((bit) => Boolean(bit)) srcUrl.pathname = finalBits.join(`/`).replace(`//`, `/`) return srcUrl.toString() } const afterEffectsPattern = /([^/]*e_sharpen[^/]*|[^/]*e_blur[^/]*)/ const insertionBitPattern = /^v\d+$/ /** * A factory function that allows you to inject a string of Cloudinary transforms into an existing Cloudinary src URL. This only exists for ergonomic reasons as it looks cleaner when repeating the various members of ResponsiveImage’s data-srcset collection. * * @param src - the Cloudinary URL to inject transforms into * @returns a function that accepts a string of transforms * * @internal * @private */ export const getSrcTransformer = (src: string) => (transformation: string) => injectTransforms(src, transformation) /** * This function contains the logic for returning srcSet and sizes attributes for a responsive image. * * @param src - A valid Cloudinary source URL. * @returns an object containing the src and srcSet attributes for the image. */ export const createSrcSet = (src: string): {src: string; srcSet: string} => { const transformSrc = getSrcTransformer(src) // TODO: useful when using for art direction… // const transformPortraitSrc = getSrcTransformer(portraitSrc) // const transformLandscapeSrc = getSrcTransformer(landscapeSrc) const fallbackSrc = transformSrc(`q_60,w_300`) return { src: fallbackSrc, srcSet: ` ${fallbackSrc} 300w, ${transformSrc(`q_60,w_600`)} 600w, ${transformSrc(`q_60,w_900`)} 900w, ${transformSrc(`q_60,w_1200`)} 1200w, ${transformSrc(`q_60,w_1600`)} 1600w, `, } } /** * This function creates src and srcSet for an image, with the srcSet targetting screen density (eg. 2x, 3x) rather than explicit image size (eg. 300w). This is useful for email templates, where the `sizes` attribute is not supported or annoying to code for. * @param src - The Cloudinary source URL to transform * @param size - width in CSS pixels at which the image will appear in the layout * @returns the transformed image src and srcSet * * @remarks * The srcSet is created by doubling the width of the image and specifies using it for 2x and 3x screen density, if available. */ export const createDensitySrcSet = (src: string, size = 200): {src: string; srcset: string} => { const baseSrc = injectTransforms(src, `w_${size}`) return { src: baseSrc, srcset: ` ${baseSrc} 1x, ${injectTransforms(src, `w_${size * 2}`)} 2x, ${injectTransforms(src, `w_${size * 3}`)} 3x `, } } /** * A sorting function to be used in the Array.prototype.sort callback to order `e_sharpen` and `e_blur` transforms last in the chain. * @param a - the first bit to compare * @param b - the second bit to compare * @returns 1, -1, or 0 indicating the required order change of the bits * @internal * @private */ export const effectsLast = (a: string, b: string) => { const aHasE = a.includes(`e_`) const bHasE = b.includes(`e_`) if (aHasE && !bHasE) return 1 if (!aHasE && bHasE) return -1 return 0 }