import React, { FC, HTMLAttributes, FunctionComponent } from 'react';
export interface CrystallizeImageVariant {
url: string;
width: number;
height?: number;
size?: number;
}
interface RichTextContent {
html?: Array;
json?: Array;
plainText?: Array;
}
export interface Props extends HTMLAttributes {
children?: FunctionComponent;
src?: string;
url?: string;
sizes?: string;
altText?: string;
alt?: string;
media?: string;
// The `html` content has higher priority than `plainText` because it has richer content.
// In case of getting both, the `html` is the one that will be displayed.
caption?: RichTextContent;
variants?: CrystallizeImageVariant[];
loading?: 'eager' | 'lazy';
_availableSizes?: number[];
_availableFormats?: string[];
}
function getVariantSrc(variant: CrystallizeImageVariant): string {
return `${variant.url} ${variant.width}w`;
}
export const Image: FC = ({ children, ...restOfAllProps }) => {
const {
src,
url,
sizes,
variants,
altText,
alt: altPassed,
caption,
className,
media,
_availableSizes,
_availableFormats,
...rest
} = restOfAllProps;
let vars = (variants || []).filter((v) => !!v);
const alt = typeof altPassed === 'string' ? altPassed : altText;
// Naive rendering POC
if (url && _availableSizes && _availableFormats) {
vars = [];
const urlWithoutFileExtension = url.replace(/\.[^/]+$/, '');
const match = urlWithoutFileExtension.match(/(.+)(\/)([^/]+)$/);
if (match) {
const [, base, , filename] = match;
_availableSizes.forEach((size) => {
_availableFormats.forEach((format) => {
vars.push({
url: `${base}/@${size}/${filename}.${format}`,
width: size,
});
});
});
}
}
const hasVariants = vars.length > 0;
// Get the biggest image from the variants
let biggestImage: CrystallizeImageVariant = vars[0];
if (hasVariants) {
biggestImage = vars.reduce(function (
acc: CrystallizeImageVariant,
v: CrystallizeImageVariant
): CrystallizeImageVariant {
if (!acc.width || v.width > acc.width) {
return v;
}
return acc;
},
vars[0]);
}
// Determine srcSet
const std = vars.filter(
(v) => v.url && !v.url.endsWith('.webp') && !v.url.endsWith('.avif')
);
const webp = vars.filter((v) => v.url && v.url.endsWith('.webp'));
const avif = vars.filter((v) => v.url && v.url.endsWith('.avif'));
const srcSet = std.map(getVariantSrc).join(', ');
const srcSetWebp = webp.map(getVariantSrc).join(', ');
const srcSetAvif = avif.map(getVariantSrc).join(', ');
// Determine the original file extension
let originalFileExtension = 'jpeg';
if (std.length > 0) {
const match = std[0].url.match(/\.(?[^.]+)$/);
originalFileExtension = match?.groups?.name || 'jpeg';
// Provide correct mime type for jpg
if (originalFileExtension === 'jpg') {
originalFileExtension = 'jpeg';
}
}
const commonProps = {
// Ensure fallback src for older browsers
src: src || url || (hasVariants ? std[0].url : undefined),
alt,
caption,
width: biggestImage?.width,
height: biggestImage?.height,
};
let useWebP = srcSetWebp.length > 0;
let useAvif = srcSetAvif.length > 0;
/**
* Only output Avif format if it is smaller than
* webP. For the future: show only one of them when
* the browser support for Avif is good enough
*/
if (useWebP && useAvif) {
const [firstWebp] = webp;
const [firstAvif] = avif;
if (firstWebp.size && firstAvif.size) {
useAvif = firstWebp.size > firstAvif.size;
}
}
if (children) {
return children({
srcSet,
srcSetWebp,
srcSetAvif,
useAvif,
useWebP,
className,
sizes,
media,
...commonProps,
...rest,
originalFileExtension,
});
}
const captionString = caption?.html?.[0] || caption?.plainText?.[0] || '';
return (
{useAvif && (
)}
{useWebP && (
)}
{srcSet.length > 0 && (
)}
{/* eslint-disable-next-line jsx-a11y/alt-text */}
{captionString && (
)}
);
};