deno.land / x / lume@v2.1.4 / plugins / picture.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371import { posix } from "../deps/path.ts";import { getPathAndExtension } from "../core/utils/path.ts";import { merge } from "../core/utils/object.ts";import { contentType } from "../deps/media_types.ts";
import type Site from "../core/site.ts";
interface SourceFormat { width?: number; scales: Record<string, number>; format: string;}
interface Source extends SourceFormat { paths: string[];}
export interface Options { /** The key name for the transformations definitions */ name?: string;
/** The priority order of the formats */ order?: string[];}
// Default optionsexport const defaults: Options = { name: "transformImages", order: ["jxl", "avif", "webp", "png", "jpg"],};
export default function (userOptions?: Options) { const options = merge(defaults, userOptions);
return (site: Site) => { const transforms = new Map<string, Source>();
site.process([".html"], (pages) => { for (const page of pages) { const { document } = page;
if (!document) { return; }
const basePath = posix.dirname(page.outputPath); const images = document.querySelectorAll("img");
for (const img of Array.from(images)) { const transformImages = img.closest("[transform-images]") ?.getAttribute( "transform-images", );
if (!transformImages) { continue; }
if (!img.getAttribute("src")) { throw new Error("img element must have a src attribute"); }
const picture = img.closest("picture");
if (picture) { handlePicture(transformImages, img, picture, basePath); continue; }
handleImg(transformImages, img, basePath); } }
// Remove the image-transform attribute from the HTML for (const page of pages) { page.document?.querySelectorAll("[transform-images]").forEach( (element) => { element.removeAttribute("transform-images"); }, ); } });
site.process("*", (pages) => { for (const page of pages) { const path = page.outputPath;
for (const { paths, width, scales, format } of transforms.values()) { if (!paths.includes(path)) { continue; }
const { name } = options; const transformImages = page.data[name] = page.data[name] ? Array.isArray(page.data[name]) ? page.data[name] : [page.data[name]] : [];
for (const [suffix, scale] of Object.entries(scales)) { if (width) { transformImages.push({ resize: width * scale, suffix, format, }); continue; }
transformImages.push({ suffix, format, }); } } } });
function handlePicture( transformImages: string, img: Element, picture: Element, basePath: string, ) { const src = img.getAttribute("src") as string; const sizes = img.getAttribute("sizes"); const sourceFormats = saveTransform(basePath, src, transformImages);
sortSources(sourceFormats); const last = sourceFormats[sourceFormats.length - 1];
for (const sourceFormat of sourceFormats) { if (sourceFormat === last) { editImg(img, src, last, sizes); break; } const source = createSource( img.ownerDocument!, src, sourceFormat, sizes, ); picture.insertBefore(source, img); } }
function handleImg( transformImages: string, img: Element, basePath: string, ) { const src = img.getAttribute("src") as string; const sizes = img.getAttribute("sizes"); const sourceFormats = saveTransform(basePath, src, transformImages);
sortSources(sourceFormats);
// Just only one format, no need to create a picture element if (sourceFormats.length === 1) { editImg(img, src, sourceFormats[0], sizes); return; }
const picture = img.ownerDocument!.createElement("picture");
img.replaceWith(picture);
const last = sourceFormats[sourceFormats.length - 1];
for (const sourceFormat of sourceFormats) { if (sourceFormat === last) { editImg(img, src, last, sizes); break; }
const source = createSource( img.ownerDocument!, src, sourceFormat, sizes, ); picture.append(source); }
picture.append(img); }
function sortSources(sources: SourceFormat[]) { const { order } = options;
sources.sort((a, b) => { const aIndex = order.indexOf(a.format); const bIndex = order.indexOf(b.format);
if (aIndex === -1) { return 1; }
if (bIndex === -1) { return -1; }
return aIndex - bIndex; }); }
function saveTransform( basePath: string, src: string, transformImages: string, ): SourceFormat[] { const path = src.startsWith("/") ? src : posix.join(basePath, src); const sizes: string[] = []; const formats: string[] = [];
transformImages.trim().split(/\s+/).forEach((piece) => { if (piece.match(/^\d/)) { sizes.push(piece); } else { formats.push(piece); } });
const sourceFormats: SourceFormat[] = [];
// No sizes, only transform to the formats if (!sizes.length) { for (const format of formats) { const key = `:${format}`; const sourceFormat = { format, scales: { "": 1 }, }; sourceFormats.push(sourceFormat);
const transform = transforms.get(key);
if (transform) { if (!transform.paths.includes(path)) { transform.paths.push(path); }
Object.assign(transform.scales, sourceFormat.scales); } else { transforms.set(key, { ...sourceFormat, paths: [path], }); } }
return sourceFormats; }
for (const size of sizes) { const [width, scales] = parseSize(size);
for (const format of formats) { const key = `${width}:${format}`; const sourceFormat = { width, format, scales: {} as Record<string, number>, }; sourceFormats.push(sourceFormat);
for (const scale of scales) { const suffix = `-${width}w${scale === 1 ? "" : `@${scale}`}`; sourceFormat.scales[suffix] = scale; }
const transform = transforms.get(key);
if (transform) { if (!transform.paths.includes(path)) { transform.paths.push(path); }
Object.assign(transform.scales, sourceFormat.scales); } else { transforms.set(key, { ...sourceFormat, paths: [path], }); } } }
return sourceFormats; } };}
function parseSize(size: string): [number, number[]] { const match = size.match(/^(\d+)(@([\d.,]+))?$/);
if (!match) { throw new Error(`Invalid size: ${size}`); }
const [, width, , scales] = match;
// Use a Set to avoid duplicates const sizes = new Set<number>([1]); scales?.split(",").forEach((size) => sizes.add(parseFloat(size)));
return [ parseInt(width), [...sizes.values()], ];}
function createSrcset( src: string, srcFormat: SourceFormat, sizes?: string | null | undefined,): string[] { const { scales, format, width } = srcFormat; const path = encodeURI(getPathAndExtension(src)[0]); const srcset: string[] = [];
for (const [suffix, scale] of Object.entries(scales)) { const scaleSuffix = sizes && width ? ` ${scale * width}w` : scale === 1 ? "" : ` ${scale}x`; srcset.push(`${path}${suffix}.${format}${scaleSuffix}`); }
return srcset;}
function createSource( document: Document, src: string, srcFormat: SourceFormat, sizes?: string | null | undefined,) { const source = document.createElement("source"); const srcset = createSrcset(src, srcFormat, sizes);
source.setAttribute("srcset", srcset.join(", ")); source.setAttribute("type", contentType(srcFormat.format) || "");
if (sizes) { source.setAttribute("sizes", sizes); }
return source;}
function editImg( img: Element, src: string, srcFormat: SourceFormat, sizes?: string | null | undefined,) { const srcset = createSrcset(src, srcFormat, sizes); const newSrc = srcset.shift()!;
if (srcset.length) { img.setAttribute("srcset", srcset.join(", ")); } img.setAttribute("src", newSrc);
if (sizes) { img.setAttribute("sizes", sizes); }}
Version Info