221 lines
7.2 KiB
JavaScript
221 lines
7.2 KiB
JavaScript
|
|
const { escapeAttribute } = require("entities");
|
|||
|
|
|
|||
|
|
const LOWSRC_FORMAT_PREFERENCE = ["jpeg", "png", "gif", "svg", "webp", "avif"];
|
|||
|
|
|
|||
|
|
const CHILDREN_OBJECT_KEY = "@children";
|
|||
|
|
|
|||
|
|
function generateSrcset(metadataFormatEntry) {
|
|||
|
|
if(!Array.isArray(metadataFormatEntry)) {
|
|||
|
|
return "";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return metadataFormatEntry.map(entry => entry.srcset).join(", ");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/*
|
|||
|
|
Returns:
|
|||
|
|
e.g. { img: { alt: "", src: "" }
|
|||
|
|
e.g. { img: { alt: "", src: "", srcset: "", sizes: "" } }
|
|||
|
|
e.g. { picture: {
|
|||
|
|
class: "",
|
|||
|
|
@children: [
|
|||
|
|
{ source: { srcset: "", sizes: "" } },
|
|||
|
|
{ source: { srcset: "", sizes: "" } },
|
|||
|
|
{ img: { alt: "", src: "", srcset: "", sizes: "" } },
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
*/
|
|||
|
|
function generateObject(metadata, userDefinedImgAttributes = {}, userDefinedPictureAttributes = {}, options = {}) {
|
|||
|
|
let htmlOptions = options?.htmlOptions || {};
|
|||
|
|
let imgAttributes = Object.assign({}, options?.defaultAttributes, htmlOptions?.imgAttributes, userDefinedImgAttributes);
|
|||
|
|
let pictureAttributes = Object.assign({}, htmlOptions?.pictureAttributes, userDefinedPictureAttributes);
|
|||
|
|
|
|||
|
|
// The attributes.src gets overwritten later on. Save it here to make the error outputs less cryptic.
|
|||
|
|
let originalSrc = imgAttributes.src;
|
|||
|
|
|
|||
|
|
if(imgAttributes.alt === undefined) {
|
|||
|
|
// You bet we throw an error on missing alt (alt="" works okay)
|
|||
|
|
throw new Error(`Missing \`alt\` attribute on eleventy-img shortcode from: ${originalSrc}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let formats = Object.keys(metadata);
|
|||
|
|
let values = Object.values(metadata);
|
|||
|
|
let entryCount = 0;
|
|||
|
|
for(let imageFormat of values) {
|
|||
|
|
entryCount += imageFormat.length;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if(entryCount === 0) {
|
|||
|
|
throw new Error("No image results found from `eleventy-img` in generateHTML. Expects a results object similar to: https://www.11ty.dev/docs/plugins/image/#usage.");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let lowsrc;
|
|||
|
|
let lowsrcFormat;
|
|||
|
|
for(let format of LOWSRC_FORMAT_PREFERENCE) {
|
|||
|
|
if((format in metadata) && metadata[format].length) {
|
|||
|
|
lowsrcFormat = format;
|
|||
|
|
lowsrc = metadata[lowsrcFormat];
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Handle if empty intersection between format and LOWSRC_FORMAT_PREFERENCE (e.g. gif)
|
|||
|
|
// If there’s only one format in the results, use that
|
|||
|
|
if(!lowsrc && formats.length === 1) {
|
|||
|
|
lowsrcFormat = formats[0];
|
|||
|
|
lowsrc = metadata[lowsrcFormat];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if(!lowsrc || !lowsrc.length) {
|
|||
|
|
throw new Error(`Could not find the lowest <img> source for responsive markup for ${originalSrc}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
imgAttributes.src = lowsrc[0].url;
|
|||
|
|
|
|||
|
|
if(htmlOptions.fallback === "largest" || htmlOptions.fallback === undefined) {
|
|||
|
|
imgAttributes.width = lowsrc[lowsrc.length - 1].width;
|
|||
|
|
imgAttributes.height = lowsrc[lowsrc.length - 1].height;
|
|||
|
|
} else if(htmlOptions.fallback === "smallest") {
|
|||
|
|
imgAttributes.width = lowsrc[0].width;
|
|||
|
|
imgAttributes.height = lowsrc[0].height;
|
|||
|
|
} else {
|
|||
|
|
throw new Error("Invalid `fallback` option specified. 'largest' and 'smallest' are supported. Received: " + htmlOptions.fallback);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let imgAttributesWithoutSizes = Object.assign({}, imgAttributes);
|
|||
|
|
delete imgAttributesWithoutSizes.sizes;
|
|||
|
|
|
|||
|
|
// <img>: one format and one size
|
|||
|
|
if(entryCount === 1) {
|
|||
|
|
return {
|
|||
|
|
img: imgAttributesWithoutSizes
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Per the HTML specification sizes is required srcset is using the `w` unit
|
|||
|
|
// https://html.spec.whatwg.org/dev/semantics.html#the-link-element:attr-link-imagesrcset-4
|
|||
|
|
// Using the default "100vw" is okay
|
|||
|
|
let missingSizesErrorMessage = `Missing \`sizes\` attribute on eleventy-img shortcode from: ${originalSrc}. Workarounds: 1. Use a single output width for this image 2. Use \`loading="lazy"\` (which uses sizes="auto" though browser support currently varies)`;
|
|||
|
|
|
|||
|
|
// <img srcset>: one format and multiple sizes
|
|||
|
|
if(formats.length === 1) { // implied entryCount > 1
|
|||
|
|
if(entryCount > 1 && !imgAttributes.sizes) {
|
|||
|
|
// Use `sizes="auto"` when using `loading="lazy"` instead of throwing an error.
|
|||
|
|
if(imgAttributes.loading === "lazy") {
|
|||
|
|
imgAttributes.sizes = "auto";
|
|||
|
|
} else {
|
|||
|
|
throw new Error(missingSizesErrorMessage);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let imgAttributesCopy = Object.assign({}, imgAttributesWithoutSizes);
|
|||
|
|
imgAttributesCopy.srcset = generateSrcset(lowsrc);
|
|||
|
|
imgAttributesCopy.sizes = imgAttributes.sizes;
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
img: imgAttributesCopy
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let children = [];
|
|||
|
|
values.filter(imageFormat => {
|
|||
|
|
return imageFormat.length > 0 && (lowsrcFormat !== imageFormat[0].format);
|
|||
|
|
}).forEach(imageFormat => {
|
|||
|
|
if(imageFormat.length > 1 && !imgAttributes.sizes) {
|
|||
|
|
if(imgAttributes.loading === "lazy") {
|
|||
|
|
imgAttributes.sizes = "auto";
|
|||
|
|
} else {
|
|||
|
|
throw new Error(missingSizesErrorMessage);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let sourceAttrs = {
|
|||
|
|
type: imageFormat[0].sourceType,
|
|||
|
|
srcset: generateSrcset(imageFormat),
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if(imgAttributes.sizes) {
|
|||
|
|
sourceAttrs.sizes = imgAttributes.sizes;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
children.push({
|
|||
|
|
"source": sourceAttrs
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/*
|
|||
|
|
Add lowsrc as an img, for browsers that don’t support picture or the formats provided in source
|
|||
|
|
|
|||
|
|
If we have more than one size, we can use srcset and sizes.
|
|||
|
|
If the browser doesn't support those attributes, it should ignore them.
|
|||
|
|
*/
|
|||
|
|
let imgAttributesForPicture = Object.assign({}, imgAttributesWithoutSizes);
|
|||
|
|
if (lowsrc.length > 1) {
|
|||
|
|
if (!imgAttributes.sizes) {
|
|||
|
|
// Per the HTML specification sizes is required srcset is using the `w` unit
|
|||
|
|
// https://html.spec.whatwg.org/dev/semantics.html#the-link-element:attr-link-imagesrcset-4
|
|||
|
|
// Using the default "100vw" is okay
|
|||
|
|
throw new Error(missingSizesErrorMessage);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
imgAttributesForPicture.srcset = generateSrcset(lowsrc);
|
|||
|
|
imgAttributesForPicture.sizes = imgAttributes.sizes;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
children.push({
|
|||
|
|
"img": imgAttributesForPicture
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"picture": {
|
|||
|
|
...pictureAttributes,
|
|||
|
|
[CHILDREN_OBJECT_KEY]: children,
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function mapObjectToHTML(tagName, attrs = {}) {
|
|||
|
|
let attrHtml = Object.entries(attrs).map(entry => {
|
|||
|
|
let [key, value] = entry;
|
|||
|
|
if(key === CHILDREN_OBJECT_KEY) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Issue #82
|
|||
|
|
if(key === "alt") {
|
|||
|
|
return `${key}="${value ? escapeAttribute(value) : ""}"`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return `${key}="${value}"`;
|
|||
|
|
}).filter(keyPair => Boolean(keyPair)).join(" ");
|
|||
|
|
|
|||
|
|
return `<${tagName}${attrHtml ? ` ${attrHtml}` : ""}>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function generateHTML(metadata, attributes = {}, htmlOptionsOverride = {}) {
|
|||
|
|
let htmlOptions = Object.assign({}, metadata?.eleventyImage?.htmlOptions, htmlOptionsOverride);
|
|||
|
|
|
|||
|
|
let isInline = htmlOptions.whitespaceMode !== "block";
|
|||
|
|
let markup = [];
|
|||
|
|
|
|||
|
|
// htmlOptions.imgAttributes and htmlOptions.pictureAttributes are merged in generateObject
|
|||
|
|
let obj = generateObject(metadata, attributes, {}, { htmlOptions });
|
|||
|
|
for(let tag in obj) {
|
|||
|
|
markup.push(mapObjectToHTML(tag, obj[tag]));
|
|||
|
|
|
|||
|
|
// <picture>
|
|||
|
|
if(Array.isArray(obj[tag]?.[CHILDREN_OBJECT_KEY])) {
|
|||
|
|
for(let child of obj[tag][CHILDREN_OBJECT_KEY]) {
|
|||
|
|
let childTagName = Object.keys(child)[0];
|
|||
|
|
markup.push((!isInline ? " " : "") + mapObjectToHTML(childTagName, child[childTagName]));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
markup.push(`</${tag}>`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return markup.join(!isInline ? "\n" : "");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = generateHTML;
|
|||
|
|
module.exports.generateObject = generateObject;
|