This commit is contained in:
2026-03-31 16:38:22 -07:00
commit 38940436a7
2112 changed files with 376929 additions and 0 deletions

40
node_modules/@11ty/eleventy-img/README.md generated vendored Normal file
View File

@ -0,0 +1,40 @@
<p align="center"><img src="https://www.11ty.dev/img/logo-github.svg" width="200" height="200" alt="eleventy Logo"></p>
# eleventy-img
Requires Node 18+
Low level utility to perform build-time image transformations for both vector and raster images. Output multiple sizes, save multiple formats, cache remote images locally. Uses the [sharp](https://sharp.pixelplumbing.com/) image processor.
You maintain full control of your HTML. Use with `<picture>` or `<img>` or CSS `background-image`, or others! Works great to add `width` and `height` to your images!
## [The full `eleventy-img` documentation is on 11ty.dev](https://www.11ty.dev/docs/plugins/image/).
* _This is a plugin for the [Eleventy static site generator](https://www.11ty.dev/)._
* Find more [Eleventy plugins](https://www.11ty.dev/docs/plugins/).
* Please star [Eleventy on GitHub](https://github.com/11ty/eleventy/), follow [@eleven_ty](https://twitter.com/eleven_ty) on Twitter, and support [11ty on Open Collective](https://opencollective.com/11ty)
[![npm Version](https://img.shields.io/npm/v/@11ty/eleventy-img.svg?style=for-the-badge)](https://www.npmjs.com/package/@11ty/eleventy-img) [![GitHub issues](https://img.shields.io/github/issues/11ty/eleventy-img.svg?style=for-the-badge)](https://github.com/11ty/eleventy-img/issues)
## Installation
```
npm install --save-dev @11ty/eleventy-img
```
_[The full `eleventy-img` documentation is on 11ty.dev](https://www.11ty.dev/docs/plugins/image/)._
## Tests
```
npm run test
```
- We use the [ava JavaScript test runner](https://github.com/avajs/ava) ([Assertions documentation](https://github.com/avajs/ava/blob/master/docs/03-assertions.md))
- To keep tests fast, thou shalt try to avoid writing files in tests.
## Community Roadmap
- [Top Feature Requests](https://github.com/11ty/eleventy-img/issues?q=label%3Aneeds-votes+sort%3Areactions-%2B1-desc+label%3Aenhancement) (Add your own votes using the 👍 reaction)
- [Top Bugs 😱](https://github.com/11ty/eleventy-img/issues?q=is%3Aissue+is%3Aopen+label%3Abug+sort%3Areactions-%2B1-desc) (Add your own votes using the 👍 reaction)
- [Newest Bugs 🙀](https://github.com/11ty/eleventy-img/issues?q=is%3Aopen+is%3Aissue+label%3Abug)

131
node_modules/@11ty/eleventy-img/eleventy-image.webc generated vendored Normal file
View File

@ -0,0 +1,131 @@
<!---
Supported attribute list:
* src (required)
* width
* formats
* url-path
* output-dir
<img
webc:is="eleventy-image"
src="./src/img/possum-geri.png"
alt="The possum is Eleventys mascot"
width="222, 350"
class="some-custom-class"
sizes="(min-width: 43.75em) 100px, 15vw">
Alternative attribute formats:
:width="[222, 350]"
formats="avif,webp,jpeg"
:formats="['avif', 'webp', 'jpeg']"
--->
<script webc:type="js">
const path = require("path");
// TODO expose this for re-use in a provided shortcode.
async function imagePlugin(attributes, globalPluginOptions) {
if(!attributes.src) {
throw new Error("Missing `src` attribute on <eleventy-image>");
}
// TODO allow remote optimization automatically on full urls
let imagePackage;
let defaultGlobalAttributes;
if(globalPluginOptions) {
defaultGlobalAttributes = globalPluginOptions.defaultAttributes;
delete globalPluginOptions.defaultAttributes;
imagePackage = globalPluginOptions.packages?.image;
delete globalPluginOptions.packages;
}
if(!imagePackage) {
imagePackage = require("@11ty/eleventy-img");
}
let instanceOptions = {};
// Note that multiple widths require a `sizes`
if(attributes.width) {
if(typeof attributes.width === "string") {
instanceOptions.widths = attributes.width.split(",").map(entry => parseInt(entry, 10));
delete attributes.width;
} else if(Array.isArray(attributes.width)) {
instanceOptions.widths = attributes.width;
delete attributes.width;
}
}
if(attributes.formats) {
if(typeof attributes.formats === "string") {
instanceOptions.formats = attributes.formats.split(",").map(entry => entry.trim());
delete attributes.formats;
} else if(Array.isArray(attributes.formats)) {
instanceOptions.formats = attributes.formats;
delete attributes.formats;
}
}
// These defaults are set only if addPlugin was **not** called:
if(!globalPluginOptions) {
// Using eleventy.directories global data (Eleventy 2.0.2+)
if(eleventy.directories) {
instanceOptions.urlPath = "/img/";
// write to output folder by default
instanceOptions.outputDir = path.join(eleventy.directories.output, instanceOptions.urlPath);
}
}
// Overrides via attributes (hopefully you dont need these)
if(attributes.urlPath) {
instanceOptions.urlPath = attributes.urlPath;
delete attributes.urlPath;
if(eleventy.directories && !attributes.outputDir) {
// use output folder if available (Eleventy v2.0.2+)
instanceOptions.outputDir = path.join(eleventy.directories.output, instanceOptions.urlPath);
}
}
if(attributes.outputDir) {
instanceOptions.outputDir = attributes.outputDir;
delete attributes.outputDir;
}
let options = Object.assign({}, globalPluginOptions, instanceOptions);
// see Util.addConfig
if(globalPluginOptions.eleventyConfig) {
Object.defineProperty(options, "eleventyConfig", {
value: globalPluginOptions.eleventyConfig,
enumerable: false,
});
}
let metadata = await imagePackage(src, options);
let imageAttributes = Object.assign({}, defaultGlobalAttributes, attributes);
// You bet we throw an error on missing alt in `imageAttributes` (alt="" works okay)
return imagePackage.generateHTML(metadata, imageAttributes);
};
(async () => {
let globalPluginOptions;
// fetch global options from from addPlugin call
if(typeof __private_eleventyImageConfigurationOptions === "function") {
globalPluginOptions = __private_eleventyImageConfigurationOptions();
}
if(!("filterPublicAttributes" in webc)) {
throw new Error("The <eleventy-image> WebC component requires WebC v0.10.1+");
}
let attributes = webc.filterPublicAttributes(webc.attributes);
return imagePlugin(attributes, globalPluginOptions);
})();
</script>

29
node_modules/@11ty/eleventy-img/eslint.config.mjs generated vendored Normal file
View File

@ -0,0 +1,29 @@
import { defineConfig } from "eslint/config";
import pluginJs from "@eslint/js";
import pluginStylistic from "@stylistic/eslint-plugin-js";
import globals from "globals";
const GLOB_JS = '**/*.?([cm])js';
export default defineConfig([
{
files: [GLOB_JS],
plugins: {
js: pluginJs,
"@stylistic/js": pluginStylistic
},
extends: [
"js/recommended",
],
languageOptions: {
ecmaVersion: 2022,
sourceType: "module",
globals: { ...globals.node },
},
rules: {
"@stylistic/js/indent": ["error", 2],
"@stylistic/js/linebreak-style": ["error", "unix"],
"@stylistic/js/semi": ["error", "always"],
},
},
]);

151
node_modules/@11ty/eleventy-img/img.js generated vendored Normal file
View File

@ -0,0 +1,151 @@
const {default: PQueue} = require("p-queue");
const DeferCounter = require("./src/defer-counter.js");
const BuildLogger = require("./src/build-logger.js");
const Util = require("./src/util.js");
const Image = require("./src/image.js");
const DirectoryManager = require("./src/directory-manager.js");
// For exports
const getImageSize = require("image-size");
const ImagePath = require("./src/image-path.js");
const debug = require("debug")("Eleventy:Image");
const GLOBAL_OPTIONS = require("./src/global-options.js").defaults;
const { memCache, diskCache } = require("./src/caches.js");
let deferCounter = new DeferCounter();
let buildLogger = new BuildLogger();
let directoryManager = new DirectoryManager();
/* Queue */
let processingQueue = new PQueue({
concurrency: GLOBAL_OPTIONS.concurrency
});
processingQueue.on("active", () => {
debug( `Concurrency: ${processingQueue.concurrency}, Size: ${processingQueue.size}, Pending: ${processingQueue.pending}` );
});
// TODO move this into build-logger.js
function setupLogger(eleventyConfig, opts) {
if(typeof eleventyConfig?.logger?.logWithOptions !== "function" || Util.isRequested(opts?.generatedVia)) {
return;
}
buildLogger.setupOnce(eleventyConfig, () => {
// before build
deferCounter.resetCount();
memCache.resetCount();
diskCache.resetCount();
}, () => {
// after build
let [memoryCacheHit] = memCache.getCount();
let [diskCacheHit, diskCacheMiss] = diskCache.getCount();
// these are unique images, multiple requests to optimize the same image are de-duplicated
let deferCount = deferCounter.getCount();
let cachedCount = memoryCacheHit + diskCacheHit;
let optimizedCount = diskCacheMiss + diskCacheHit + memoryCacheHit + deferCount;
let msg = [];
msg.push(`${optimizedCount} ${optimizedCount !== 1 ? "images" : "image"} optimized`);
if(cachedCount > 0 || deferCount > 0) {
let innerMsg = [];
if(cachedCount > 0) {
innerMsg.push(`${cachedCount} cached`);
}
if(deferCount > 0) {
innerMsg.push(`${deferCount} deferred`);
}
msg.push(` (${innerMsg.join(", ")})`);
}
if(optimizedCount > 0 || cachedCount > 0 || deferCount > 0) {
eleventyConfig?.logger?.logWithOptions({
message: msg.join(""),
prefix: "[11ty/eleventy-img]",
color: "green",
});
}
});
}
function createImage(src, opts = {}) {
let eleventyConfig = opts.eleventyConfig;
if(opts?.eleventyConfig && {}.propertyIsEnumerable.call(opts, "eleventyConfig")) {
delete opts.eleventyConfig;
Util.addConfig(eleventyConfig, opts);
}
let img = Image.create(src, opts);
img.setQueue(processingQueue);
img.setBuildLogger(buildLogger);
img.setDirectoryManager(directoryManager);
setupLogger(eleventyConfig, opts);
if(opts.transformOnRequest) {
deferCounter.increment(src);
}
return img;
};
function queueImage(src, opts = {}) {
if(src.constructor?.name === "UserConfig") {
throw new Error(`Eleventy Images default export is not an Eleventy Plugin and cannot be used with \`eleventyConfig.addPlugin()\`. Use the \`eleventyImageTransformPlugin\` named export instead, like this: \`import { eleventyImageTransformPlugin } from '@11ty/eleventy-img';\` or this: \`const { eleventyImageTransformPlugin } = require('@11ty/eleventy-img');\``);
}
let img = createImage(src, opts);
return img.queue();
}
// Exports
module.exports = queueImage;
Object.defineProperty(module.exports, "concurrency", {
get: function() {
return processingQueue.concurrency;
},
set: function(concurrency) {
processingQueue.concurrency = concurrency;
},
});
module.exports.Util = Util;
module.exports.Image = Image;
module.exports.ImagePath = ImagePath;
module.exports.ImageSize = getImageSize;
// Backwards compat
module.exports.statsSync = Image.statsSync;
module.exports.statsByDimensionsSync = Image.statsByDimensionsSync;
module.exports.getFormats = Image.getFormatsArray;
module.exports.getWidths = Image.getValidWidths;
module.exports.getHash = function getHash(src, options) {
let img = new Image(src, options);
return img.getHash();
};
module.exports.setupLogger = setupLogger;
const generateHTML = require("./src/generate-html.js");
module.exports.generateHTML = generateHTML;
module.exports.generateObject = generateHTML.generateObject;
const { eleventyWebcOptionsPlugin } = require("./src/webc-options-plugin.js");
module.exports.eleventyImagePlugin = eleventyWebcOptionsPlugin;
module.exports.eleventyImageWebcOptionsPlugin = eleventyWebcOptionsPlugin;
const { eleventyImageTransformPlugin } = require("./src/transform-plugin.js");
module.exports.eleventyImageTransformPlugin = eleventyImageTransformPlugin;
const { eleventyImageOnRequestDuringServePlugin } = require("./src/on-request-during-serve-plugin.js");
module.exports.eleventyImageOnRequestDuringServePlugin = eleventyImageOnRequestDuringServePlugin;

75
node_modules/@11ty/eleventy-img/package.json generated vendored Normal file
View File

@ -0,0 +1,75 @@
{
"name": "@11ty/eleventy-img",
"version": "6.0.4",
"description": "Low level utility to perform build-time image transformations.",
"publishConfig": {
"access": "public"
},
"main": "img.js",
"engines": {
"node": ">=18"
},
"scripts": {
"pretest": "eslint img.js src/**.js test/**.js",
"test": "ava --no-worker-threads",
"watch": "ava --no-worker-threads --watch",
"sample": "cd sample && node sample.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/11ty/eleventy-img.git"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/11ty"
},
"keywords": [
"eleventy",
"eleventy-utility"
],
"author": {
"name": "Zach Leatherman",
"email": "zachleatherman@gmail.com",
"url": "https://zachleat.com/"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/11ty/eleventy-img/issues"
},
"homepage": "https://github.com/11ty/eleventy-img#readme",
"dependencies": {
"@11ty/eleventy-fetch": "^5.1.0",
"@11ty/eleventy-utils": "^2.0.7",
"brotli-size": "^4.0.0",
"debug": "^4.4.0",
"entities": "^6.0.0",
"image-size": "^1.2.1",
"p-queue": "^6.6.2",
"sharp": "^0.33.5"
},
"devDependencies": {
"@11ty/eleventy": "^3.0.0",
"@11ty/eleventy-plugin-webc": "^0.11.2",
"@eslint/js": "^9.26.0",
"@stylistic/eslint-plugin-js": "^4.2.0",
"ava": "^6.3.0",
"eslint": "^9.26.0",
"exifr": "^7.1.3",
"globals": "^16.1.0",
"pixelmatch": "^5.3.0"
},
"ava": {
"failFast": false,
"files": [
"./test/*.{js,cjs,mjs}"
],
"watchMode": {
"ignoreChanges": [
"./.cache/*",
"./img/*",
"./test/img/*",
"./test/**/generated*"
]
}
}
}

View File

@ -0,0 +1,3 @@
module.exports = function() {
throw new Error("`svgCompressionSize: 'br'` feature is not supported in browser.");
};

View File

@ -0,0 +1,5 @@
const brotliSize = require("brotli-size");
module.exports = function(contents) {
return brotliSize.sync(contents);
};

View File

@ -0,0 +1,3 @@
module.exports = function() {
throw new Error("Sharp is not supported in browser.");
};

View File

@ -0,0 +1,3 @@
const sharp = require("sharp");
module.exports = sharp;

66
node_modules/@11ty/eleventy-img/src/build-logger.js generated vendored Normal file
View File

@ -0,0 +1,66 @@
const path = require("node:path");
const { TemplatePath } = require("@11ty/eleventy-utils");
const Util = require("./util.js");
class BuildLogger {
#eleventyConfig;
constructor() {
this.hasAssigned = false;
}
setupOnce(eleventyConfig, beforeCallback, afterCallback) {
if(this.hasAssigned) {
return;
}
this.hasAssigned = true;
this.#eleventyConfig = eleventyConfig;
eleventyConfig.on("eleventy.before", beforeCallback);
eleventyConfig.on("eleventy.after", afterCallback);
eleventyConfig.on("eleventy.reset", () => {
this.hasAssigned = false;
beforeCallback(); // we run this on reset because the before callback will have disappeared (as the config reset)
});
}
getFriendlyImageSource(imageSource) {
if(Buffer.isBuffer(imageSource)) {
return `<Buffer>`;
}
if(Util.isRemoteUrl(imageSource)) {
return imageSource;
}
if(path.isAbsolute(imageSource)) {
// convert back to relative url
return TemplatePath.addLeadingDotSlash(path.relative(path.resolve("."), imageSource));
}
return TemplatePath.addLeadingDotSlash(imageSource);
}
log(message, options = {}, logOptions = {}) {
if(typeof this.#eleventyConfig?.logger?.logWithOptions !== "function" || options.transformOnRequest) {
return;
}
this.#eleventyConfig.logger.logWithOptions(Object.assign({
message: `${message}${options.generatedVia ? ` (${options.generatedVia})` : ""}`,
type: "log",
prefix: "[11ty/eleventy-img]"
}, logOptions));
}
error(message, options = {}, logOptions = {}) {
logOptions.type = "error";
logOptions.force = true;
this.log(message, options, logOptions);
}
}
module.exports = BuildLogger;

16
node_modules/@11ty/eleventy-img/src/caches.js generated vendored Normal file
View File

@ -0,0 +1,16 @@
const MemoryCache = require("./memory-cache.js");
const DiskCache = require("./disk-cache.js");
const ExistsCache = require("./exists-cache.js");
let memCache = new MemoryCache();
let existsCache = new ExistsCache();
let diskCache = new DiskCache();
diskCache.setExistsCache(existsCache);
module.exports = {
memCache,
diskCache,
existsCache
};

27
node_modules/@11ty/eleventy-img/src/defer-counter.js generated vendored Normal file
View File

@ -0,0 +1,27 @@
class DeferCounter {
constructor() {
this.resetCount();
}
resetCount() {
this.deferCount = 0;
this.inputs = new Map();
}
getCount() {
return this.deferCount;
}
increment(input) {
if(input) {
if(this.inputs.has(input)) {
return;
}
this.inputs.set(input, true);
}
this.deferCount++;
}
}
module.exports = DeferCounter;

View File

@ -0,0 +1,29 @@
const fs = require("node:fs");
const path = require("node:path");
const debugUtil = require("debug");
const debugAssets = debugUtil("Eleventy:Assets");
class DirectoryManager {
#dirs = new Set();
isCreated(dir) {
return this.#dirs.has(dir);
}
create(dir) {
if(this.isCreated(dir)) {
return;
}
this.#dirs.add(dir);
debugAssets("[11ty/eleventy-img] Creating directory %o", dir);
fs.mkdirSync(dir, { recursive: true });
}
createFromFile(filepath) {
let dir = path.dirname(filepath);
this.create(dir);
}
}
module.exports = DirectoryManager;

54
node_modules/@11ty/eleventy-img/src/disk-cache.js generated vendored Normal file
View File

@ -0,0 +1,54 @@
// const debug = require("debug")("Eleventy:Image");
class DiskCache {
#existsCache;
constructor() {
this.hitCounter = 0;
this.missCounter = 0;
this.inputs = new Map();
}
setExistsCache(existsCache) {
this.#existsCache = existsCache;
}
resetCount() {
this.hitCounter = 0;
this.missCounter = 0;
}
getCount() {
return [this.hitCounter, this.missCounter];
}
isCached(targetFile, sourceInput, incrementCounts = true) {
if(!this.#existsCache) {
throw new Error("Missing `#existsCache`");
}
// Disk cache runs once per output file, so we only increment counts once per input
if(this.inputs.has(sourceInput)) {
incrementCounts = false;
}
this.inputs.set(sourceInput, true);
if(this.#existsCache?.exists(targetFile)) {
if(incrementCounts) {
this.hitCounter++;
}
// debug("Images re-used (via disk cache): %o", this.hitCounter);
return true;
}
if(incrementCounts) {
this.missCounter++;
}
return false;
}
}
module.exports = DiskCache;

40
node_modules/@11ty/eleventy-img/src/exists-cache.js generated vendored Normal file
View File

@ -0,0 +1,40 @@
const fs = require("node:fs");
const Util = require("./util.js");
// Checks both files and directories
class ExistsCache {
#exists = new Map();
constructor() {
this.lookupCount = 0;
}
get size() {
return this.#exists.size;
}
has(path) {
return this.#exists.has(path);
}
// Relative paths (to root directory) expected (but not enforced due to perf costs)
exists(path) {
if(Util.isFullUrl(path)) {
return false;
}
if (!this.#exists.has(path)) {
let exists = fs.existsSync(path);
this.lookupCount++;
// mark for next time
this.#exists.set(path, Boolean(exists));
return exists;
}
return this.#exists.get(path);
}
}
module.exports = ExistsCache;

View File

@ -0,0 +1,14 @@
const fs = require("node:fs");
const debugUtil = require("debug");
const debugAssets = debugUtil("Eleventy:Assets");
module.exports = async function createSvg(sharpInstance) {
let input = sharpInstance.options.input;
let svgBuffer = input.buffer;
if(svgBuffer) { // remote URL already has buffer
return svgBuffer;
} else { // local file system
debugAssets("[11ty/eleventy-img] Reading %o", input.file);
return fs.readFileSync(input.file);
}
};

220
node_modules/@11ty/eleventy-img/src/generate-html.js generated vendored Normal file
View File

@ -0,0 +1,220 @@
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 theres 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 dont 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;

121
node_modules/@11ty/eleventy-img/src/global-options.js generated vendored Normal file
View File

@ -0,0 +1,121 @@
const path = require("node:path");
const os = require("node:os");
const Util = require("./util.js");
const svgHook = require("./format-hooks/svg.js");
const DEFAULTS = {
widths: ["auto"],
formats: ["webp", "jpeg"], // "png", "svg", "avif"
formatFiltering: ["transparent", "animated"],
// Via https://github.com/11ty/eleventy-img/issues/258
concurrency: Math.min(Math.max(8, os.availableParallelism()), 16),
urlPath: "/img/",
outputDir: "img/",
// true to skip raster formats if SVG input is found
// "size" to skip raster formats if larger than SVG input
svgShortCircuit: false,
svgAllowUpscale: true,
svgCompressionSize: "", // "br" to report SVG `size` property in metadata as Brotli compressed
// overrideInputFormat: false, // internal, used to force svg output in statsSync et al
sharpOptions: {}, // options passed to the Sharp constructor
sharpWebpOptions: {}, // options passed to the Sharp webp output method
sharpPngOptions: {}, // options passed to the Sharp png output method
sharpJpegOptions: {}, // options passed to the Sharp jpeg output method
sharpAvifOptions: {}, // options passed to the Sharp avif output method
formatHooks: {
svg: svgHook,
},
cacheDuration: "1d", // deprecated, use cacheOptions.duration
// disk cache for remote assets
cacheOptions: {
// duration: "1d",
// directory: ".cache",
// removeUrlQueryParams: false,
// fetchOptions: {},
},
filenameFormat: null,
// urlFormat allows you to return a full URL to an image including the domain.
// Useful when youre using your own hosted image service (probably via .statsSync or .statsByDimensionsSync)
// Note: when you use this, metadata will not include .filename or .outputPath
urlFormat: null,
// If true, skips all image processing, just return stats. Doesnt read files, doesnt write files.
// Important to note that `dryRun: true` performs image processing and includes a buffer—this does not.
// Useful when used with `urlFormat` above.
// Better than .statsSync* functions, because this will use the in-memory cache and de-dupe requests. Those will not.
statsOnly: false,
remoteImageMetadata: {}, // For `statsOnly` remote images, this needs to be populated with { width, height, format? }
useCache: true, // in-memory and disk cache
dryRun: false, // Also returns a buffer instance in the return object. Doesnt write anything to the file system
hashLength: 10, // Truncates the hash to this length
fixOrientation: false, // always rotate images to ensure correct orientation
// When the original width is smaller than the desired output width, this is the minimum size difference
// between the next smallest image width that will generate one extra width in the output.
// e.g. when using `widths: [400, 800]`, the source image would need to be at least (400 * 1.25 =) 500px wide
// to generate two outputs (400px, 500px). If the source image is less than 500px, only one output will
// be generated (400px).
// Read more at https://github.com/11ty/eleventy-img/issues/184 and https://github.com/11ty/eleventy-img/pull/190
minimumThreshold: 1.25,
// During --serve mode in Eleventy, this will generate images on request instead of part of the build skipping
// writes to the file system and speeding up builds!
transformOnRequest: false,
// operate on Sharp instance manually.
transform: undefined,
// return HTML from generateHTML directly
returnType: "object", // or "html"
// Defaults used when generateHTML is called from a result set
htmlOptions: {
imgAttributes: {},
pictureAttributes: {},
whitespaceMode: "inline", // "block"
// the <img> will use the largest dimensions for width/height (when multiple output widths are specified)
// see https://github.com/11ty/eleventy-img/issues/63
fallback: "largest", // or "smallest"
},
// v5.0.0 Removed `extensions`, option to override output format with new file extension. It wasnt being used anywhere or documented.
// v6.0.0, removed `useCacheValidityInHash: true` see https://github.com/11ty/eleventy-img/issues/146#issuecomment-2555741376
};
function getGlobalOptions(eleventyConfig, options, via) {
let directories = eleventyConfig.directories;
let globalOptions = Object.assign({
packages: {
image: require("../"),
},
outputDir: path.join(directories.output, options.urlPath || ""),
failOnError: true,
}, options);
globalOptions.directories = directories;
globalOptions.generatedVia = via;
Util.addConfig(eleventyConfig, globalOptions);
return globalOptions;
}
module.exports = {
getGlobalOptions,
defaults: DEFAULTS,
};

View File

@ -0,0 +1,140 @@
const eleventyImage = require("../img.js");
const Util = require("./util.js");
const ATTR_PREFIX = "eleventy:";
const CHILDREN_OBJECT_KEY = "@children";
const ATTR = {
IGNORE: `${ATTR_PREFIX}ignore`,
WIDTHS: `${ATTR_PREFIX}widths`,
FORMATS: `${ATTR_PREFIX}formats`,
OUTPUT: `${ATTR_PREFIX}output`,
OPTIONAL: `${ATTR_PREFIX}optional`,
PICTURE: `${ATTR_PREFIX}pictureattr:`,
};
function getPictureAttributesFromImgNode(attrs = {}) {
let pictureAttrs = {};
for(let key in attrs) {
// <img eleventy:pictureattr:NAME="VALUE"> hoists to `<picture NAME="VALUE">
// e.g. <img eleventy:pictureattr:class="outer"> hoists to <picture class="outer">
if(key.startsWith(ATTR.PICTURE)) {
pictureAttrs[key.slice(ATTR.PICTURE.length)] = attrs[key];
}
}
return pictureAttrs;
}
function convertToPosthtmlNode(obj) {
// node.tag
// node.attrs
// node.content
let node = {};
let [key] = Object.keys(obj);
node.tag = key;
let children = obj[key]?.[CHILDREN_OBJECT_KEY];
let attributes = {};
for(let attrKey in obj[key]) {
if(attrKey !== CHILDREN_OBJECT_KEY) {
attributes[attrKey] = obj[key][attrKey];
}
}
node.attrs = attributes;
if(Array.isArray(children)) {
node.content = obj[key]?.[CHILDREN_OBJECT_KEY]
.filter(child => Boolean(child))
.map(child => {
return convertToPosthtmlNode(child);
});
}
return node;
}
function isValidSimpleWidthAttribute(width) {
// `width` must be a single integer (not comma separated). Dont use invalid HTML in width attribute. Use eleventy:widths if you want more complex support
return (""+width) == (""+parseInt(width, 10));
}
async function imageAttributesToPosthtmlNode(attributes, instanceOptions, globalPluginOptions) {
if(!attributes.src) {
throw new Error("Missing `src` attribute for `@11ty/eleventy-img`");
}
if(!globalPluginOptions) {
throw new Error("Missing global defaults for `@11ty/eleventy-img`: did you call addPlugin?");
}
if(!instanceOptions) {
instanceOptions = {};
}
// overrides global widths
if(attributes.width && isValidSimpleWidthAttribute(attributes.width)) {
// Support `width` but only single value
instanceOptions.widths = [ parseInt(attributes.width, 10) ];
} else if(attributes[ATTR.WIDTHS] && typeof attributes[ATTR.WIDTHS] === "string") {
instanceOptions.widths = attributes[ATTR.WIDTHS].split(",").map(entry => parseInt(entry, 10));
}
if(attributes[ATTR.FORMATS] && typeof attributes[ATTR.FORMATS] === "string") {
instanceOptions.formats = attributes[ATTR.FORMATS].split(",");
}
let options = Object.assign({}, globalPluginOptions, instanceOptions);
Util.addConfig(globalPluginOptions.eleventyConfig, options);
let metadata = await eleventyImage(attributes.src, options);
let pictureAttributes = getPictureAttributesFromImgNode(attributes);
cleanAttrs(attributes);
// You bet we throw an error on missing alt in `imageAttributes` (alt="" works okay)
let obj = await eleventyImage.generateObject(metadata, attributes, pictureAttributes, options);
return convertToPosthtmlNode(obj);
}
function cleanAttrs(attrs = {}) {
for(let key in attrs) {
if(key.startsWith(ATTR_PREFIX)) {
delete attrs?.[key];
}
}
}
function cleanTag(node) {
// Delete all prefixed attributes
cleanAttrs(node?.attrs);
}
function isIgnored(node) {
return node?.attrs && node?.attrs?.[ATTR.IGNORE] !== undefined;
}
function isOptional(node, comparisonValue) {
let attrValue = node?.attrs && node?.attrs?.[ATTR.OPTIONAL];
if(attrValue !== undefined) {
// if comparisonValue is not specified, return true
if(comparisonValue === undefined) {
return true;
}
return attrValue === comparisonValue;
}
return false;
}
function getOutputDirectory(node) {
return node?.attrs?.[ATTR.OUTPUT];
}
module.exports = {
imageAttributesToPosthtmlNode,
cleanTag,
isIgnored,
isOptional,
getOutputDirectory,
};

30
node_modules/@11ty/eleventy-img/src/image-path.js generated vendored Normal file
View File

@ -0,0 +1,30 @@
const path = require("node:path");
class ImagePath {
static filenameFormat(id, src, width, format) { // and options
if (width) {
return `${id}-${width}.${format}`;
}
return `${id}.${format}`;
}
static getFilename(id, src, width, format, options = {}) {
if (typeof options.filenameFormat === "function") {
let filename = options.filenameFormat(id, src, width, format, options);
// if options.filenameFormat returns falsy, use fallback filename
if(filename) {
return filename;
}
}
return ImagePath.filenameFormat(id, src, width, format, options);
}
static convertFilePathToUrl(dir, filename) {
let src = path.join(dir, filename);
return src.split(path.sep).join("/");
}
}
module.exports = ImagePath;

930
node_modules/@11ty/eleventy-img/src/image.js generated vendored Normal file
View File

@ -0,0 +1,930 @@
const fs = require("node:fs");
const fsp = fs.promises;
const path = require("node:path");
const getImageSize = require("image-size");
const debugUtil = require("debug");
const { createHashSync } = require("@11ty/eleventy-utils");
const { Fetch } = require("@11ty/eleventy-fetch");
const sharp = require("./adapters/sharp.js");
const brotliSize = require("./adapters/brotli-size.js");
const Util = require("./util.js");
const ImagePath = require("./image-path.js");
const generateHTML = require("./generate-html.js");
const GLOBAL_OPTIONS = require("./global-options.js").defaults;
const { existsCache, memCache, diskCache } = require("./caches.js");
const debug = debugUtil("Eleventy:Image");
const debugAssets = debugUtil("Eleventy:Assets");
const MIME_TYPES = {
"jpeg": "image/jpeg",
"webp": "image/webp",
"png": "image/png",
"svg": "image/svg+xml",
"avif": "image/avif",
"gif": "image/gif",
};
const FORMAT_ALIASES = {
"jpg": "jpeg",
// if youre working from a mime type input, lets alias it back to svg
"svg+xml": "svg",
};
const ANIMATED_TYPES = [
"webp",
"gif",
];
const TRANSPARENCY_TYPES = [
"avif",
"png",
"webp",
"gif",
"svg",
];
const MINIMUM_TRANSPARENCY_TYPES = [
"png",
"gif",
"svg",
];
class Image {
#input;
#contents = {};
#queue;
#queuePromise;
#buildLogger;
#computedHash;
#directoryManager;
constructor(src, options = {}) {
if(!src) {
throw new Error("`src` is a required argument to the eleventy-img utility (can be a String file path, String URL, or Buffer).");
}
this.src = src;
this.isRemoteUrl = typeof src === "string" && Util.isRemoteUrl(src);
this.rawOptions = options;
this.options = Object.assign({}, GLOBAL_OPTIONS, options);
// Compatible with eleventy-dev-server and Eleventy 3.0.0-alpha.7+ in serve mode.
if(this.options.transformOnRequest && !this.options.urlFormat) {
this.options.urlFormat = function({ src, width, format }/*, imageOptions*/, options) {
return `/.11ty/image/?src=${encodeURIComponent(src)}&width=${width}&format=${format}${options.generatedVia ? `&via=${options.generatedVia}` : ""}`;
};
this.options.statsOnly = true;
}
if(this.isRemoteUrl) {
this.cacheOptions = Object.assign({
type: "buffer",
// deprecated in Eleventy Image, but we already prefer this.cacheOptions.duration automatically
duration: this.options.cacheDuration,
// Issue #117: re-use eleventy-img dryRun option value for eleventy-fetch dryRun
dryRun: this.options.dryRun,
}, this.options.cacheOptions);
// v6.0.0 this now inherits eleventy-fetch option defaults
this.assetCache = Fetch(src, this.cacheOptions);
}
}
setQueue(queue) {
this.#queue = queue;
}
setBuildLogger(buildLogger) {
this.#buildLogger = buildLogger;
}
setDirectoryManager(manager) {
this.#directoryManager = manager;
}
get directoryManager() {
if(!this.#directoryManager) {
throw new Error("Missing #directoryManager");
}
return this.#directoryManager;
}
get buildLogger() {
if(!this.#buildLogger) {
throw new Error("Missing #buildLogger. Call `setBuildLogger`");
}
return this.#buildLogger;
}
// In memory cache is up front, handles promise de-duping from input (this does not use getHash)
// Note: output cache is also in play below (uses getHash)
getInMemoryCacheKey() {
let opts = Util.getSortedObject(this.options);
opts.__originalSrc = this.src;
if(this.isRemoteUrl) {
opts.sourceUrl = this.src; // the source url
} else if(Buffer.isBuffer(this.src)) {
opts.sourceUrl = this.src.toString();
opts.__originalSize = this.src.length;
} else {
// Important: do not cache this
opts.__originalSize = fs.statSync(this.src).size;
}
return JSON.stringify(opts, function(key, value) {
// allows `transform` functions to be truthy for in-memory key
if (typeof value === "function") {
return "<fn>" + (value.name || "");
}
return value;
});
}
getFileContents(overrideLocalFilePath) {
if(!overrideLocalFilePath && this.isRemoteUrl) {
return false;
}
let src = overrideLocalFilePath || this.src;
if(!this.#contents[src]) {
// perf: check to make sure its not a string first
if(typeof src !== "string" && Buffer.isBuffer(src)) {
this.#contents[src] = src;
} else {
debugAssets("[11ty/eleventy-img] Reading %o", src);
this.#contents[src] = fs.readFileSync(src);
}
}
// Always <Buffer>
return this.#contents[src];
}
static getValidWidths(originalWidth, widths = [], allowUpscale = false, minimumThreshold = 1) {
// replace any falsy values with the original width
let valid = widths.map(width => !width || width === 'auto' ? originalWidth : width);
// Convert strings to numbers, "400" (floats are not allowed in sharp)
valid = valid.map(width => parseInt(width, 10));
// Replace any larger-than-original widths with the original width if upscaling is not allowed.
// This ensures that if a larger width has been requested, we're at least providing the closest
// non-upscaled image that we can.
if (!allowUpscale) {
let lastWidthWasBigEnough = true; // first one is always valid
valid = valid.sort((a, b) => a - b).map(width => {
if(width > originalWidth) {
if(lastWidthWasBigEnough) {
return originalWidth;
}
return -1;
}
lastWidthWasBigEnough = originalWidth > Math.floor(width * minimumThreshold);
return width;
}).filter(width => width > 0);
}
// Remove duplicates (e.g., if null happens to coincide with an explicit width
// or a user passes in multiple duplicate values, or multiple larger-than-original
// widths have resulted in the original width being included multiple times)
valid = [...new Set(valid)];
// sort ascending
return valid.sort((a, b) => a - b);
}
static getFormatsArray(formats, autoFormat, svgShortCircuit, isAnimated, hasTransparency) {
if(formats && formats.length) {
if(typeof formats === "string") {
formats = formats.split(",");
}
formats = formats.map(format => {
if(autoFormat) {
if((!format || format === "auto")) {
format = autoFormat;
}
}
if(FORMAT_ALIASES[format]) {
return FORMAT_ALIASES[format];
}
return format;
});
if(svgShortCircuit !== "size") {
// svg must come first for possible short circuiting
formats.sort((a, b) => {
if(a === "svg") {
return -1;
} else if(b === "svg") {
return 1;
}
return 0;
});
}
if(isAnimated) {
let validAnimatedFormats = formats.filter(f => ANIMATED_TYPES.includes(f));
// override formats if a valid animated format is found, otherwise leave as-is
if(validAnimatedFormats.length > 0) {
debug("Filtering non-animated formats from output: from %o to %o", formats, validAnimatedFormats);
formats = validAnimatedFormats;
} else {
debug("No animated output formats found for animated image, using original formats (may be a static image): %o", formats);
}
}
if(hasTransparency) {
let minimumValidTransparencyFormats = formats.filter(f => MINIMUM_TRANSPARENCY_TYPES.includes(f));
// override formats if a valid animated format is found, otherwise leave as-is
if(minimumValidTransparencyFormats.length > 0) {
let validTransparencyFormats = formats.filter(f => TRANSPARENCY_TYPES.includes(f));
debug("Filtering non-transparency-friendly formats from output: from %o to %o", formats, validTransparencyFormats);
formats = validTransparencyFormats;
} else {
debug("At least one transparency-friendly output format of %o must be included if the source image has an alpha channel, skipping formatFiltering and using original formats: %o", MINIMUM_TRANSPARENCY_TYPES, formats);
}
}
// Remove duplicates (e.g., if null happens to coincide with an explicit format
// or a user passes in multiple duplicate values)
formats = [...new Set(formats)];
return formats;
}
return [];
}
#transformRawFiles(files = []) {
let byType = {};
for(let file of files) {
if(!byType[file.format]) {
byType[file.format] = [];
}
byType[file.format].push(file);
}
for(let type in byType) {
// sort by width, ascending (for `srcset`)
byType[type].sort((a, b) => {
return a.width - b.width;
});
}
let filterLargeRasterImages = this.options.svgShortCircuit === "size";
let svgEntry = byType.svg;
let svgSize = svgEntry && svgEntry.length && svgEntry[0].size;
if(filterLargeRasterImages && svgSize) {
for(let type of Object.keys(byType)) {
if(type === "svg") {
continue;
}
let svgAdded = false;
let originalFormatKept = false;
byType[type] = byType[type].map(entry => {
if(entry.size > svgSize) {
if(!svgAdded) {
svgAdded = true;
// need at least one raster smaller than SVG to do this trick
if(originalFormatKept) {
return svgEntry[0];
}
// all rasters are bigger
return false;
}
return false;
}
originalFormatKept = true;
return entry;
}).filter(entry => entry);
}
}
return byType;
}
#finalizeResults(results = {}) {
// used when results are passed to generate HTML, we maintain some internal metadata about the options used.
let imgAttributes = this.options.htmlOptions?.imgAttributes || {};
imgAttributes.src = this.src;
Object.defineProperty(results, "eleventyImage", {
enumerable: false,
writable: false,
value: {
htmlOptions: {
whitespaceMode: this.options.htmlOptions?.whitespaceMode,
imgAttributes,
pictureAttributes: this.options.htmlOptions?.pictureAttributes,
fallback: this.options.htmlOptions?.fallback,
},
}
});
// renamed `return` to `returnType` to match Fetch API in v6.0.0-beta.3
if(this.options.returnType === "html" || this.options.return === "html") {
return generateHTML(results);
}
return results;
}
getSharpOptionsForFormat(format) {
if(format === "webp") {
return this.options.sharpWebpOptions;
} else if(format === "jpeg") {
return this.options.sharpJpegOptions;
} else if(format === "png") {
return this.options.sharpPngOptions;
} else if(format === "avif") {
return this.options.sharpAvifOptions;
}
return {};
}
async getInput() {
// internal cache
if(!this.#input) {
if(this.isRemoteUrl) {
// fetch remote image Buffer
this.#input = this.assetCache.queue();
} else {
// not actually a promise, this is sync
this.#input = this.getFileContents();
}
}
return this.#input;
}
getHash() {
if (this.#computedHash) {
return this.#computedHash;
}
// debug("Creating hash for %o", this.src);
let hashContents = [];
if(existsCache.exists(this.src)) {
let fileContents = this.getFileContents();
// If the file starts with whitespace or the '<' character, it might be SVG.
// Otherwise, skip the expensive buffer.toString() call
// (no point in unicode encoding a binary file)
let fileContentsPrefix = fileContents?.slice(0, 1)?.toString()?.trim();
if (!fileContentsPrefix || fileContentsPrefix[0] == "<") {
// remove all newlines for hashing for better cross-OS hash compatibility (Issue #122)
let fileContentsStr = fileContents.toString();
let firstFour = fileContentsStr.trim().slice(0, 5);
if(firstFour === "<svg " || firstFour === "<?xml") {
fileContents = fileContentsStr.replace(/\r|\n/g, '');
}
}
hashContents.push(fileContents);
} else {
// probably a remote URL
hashContents.push(this.src);
// `useCacheValidityInHash` was removed in v6.0.0, but well keep this as part of the hash to maintain consistent hashes across versions
if(this.isRemoteUrl && this.assetCache && this.cacheOptions) {
hashContents.push(`ValidCache:true`);
}
}
// We ignore all keys not relevant to the file processing/output (including `widths`, which is a suffix added to the filename)
// e.g. `widths: [300]` and `widths: [300, 600]`, with all else being equal the 300px output of each should have the same hash
let keysToKeep = [
"sharpOptions",
"sharpWebpOptions",
"sharpPngOptions",
"sharpJpegOptions",
"sharpAvifOptions"
].sort();
let hashObject = {};
// The code currently assumes are keysToKeep are Object literals (see Util.getSortedObject)
for(let key of keysToKeep) {
if(this.options[key]) {
hashObject[key] = Util.getSortedObject(this.options[key]);
}
}
hashContents.push(JSON.stringify(hashObject));
let base64hash = createHashSync(...hashContents);
let truncated = base64hash.substring(0, this.options.hashLength);
this.#computedHash = truncated;
return truncated;
}
getStat(outputFormat, width, height) {
let url;
let outputFilename;
if(this.options.urlFormat && typeof this.options.urlFormat === "function") {
let hash;
if(!this.options.statsOnly) {
hash = this.getHash();
}
url = this.options.urlFormat({
hash,
src: this.src,
width,
format: outputFormat,
}, this.options);
} else {
let hash = this.getHash();
outputFilename = ImagePath.getFilename(hash, this.src, width, outputFormat, this.options);
if(Util.isFullUrl(this.options.urlPath)) {
url = new URL(outputFilename, this.options.urlPath).toString();
} else {
url = ImagePath.convertFilePathToUrl(this.options.urlPath, outputFilename);
}
}
let statEntry = {
format: outputFormat,
width: width,
height: height,
url: url,
sourceType: MIME_TYPES[outputFormat],
srcset: `${url} ${width}w`,
// Not available in stats* functions below
// size // only after processing
};
if(outputFilename) {
statEntry.filename = outputFilename; // optional
statEntry.outputPath = path.join(this.options.outputDir, outputFilename); // optional
}
return statEntry;
}
// https://jdhao.github.io/2019/07/31/image_rotation_exif_info/
// Orientations 5 to 8 mean image is rotated ±90º (width/height are flipped)
needsRotation(orientation) {
// Sharp's metadata API exposes undefined EXIF orientations >8 as 1 (normal) but check anyways
return orientation >= 5 && orientation <= 8;
}
isAnimated(metadata) {
// sharp options have animated image support enabled
if(!this.options?.sharpOptions?.animated) {
return false;
}
let isAnimationFriendlyFormat = ANIMATED_TYPES.includes(metadata.format);
if(!isAnimationFriendlyFormat) {
return false;
}
if(metadata?.pages) {
// input has multiple pages: https://sharp.pixelplumbing.com/api-input#metadata
// this is *unknown* when not called from `resize` (limited metadata available)
return metadata?.pages > 1;
}
// Best guess
return isAnimationFriendlyFormat;
}
getEntryFormat(metadata) {
return metadata.format || this.options.overrideInputFormat;
}
// metadata so far: width, height, format
// src is used to calculate the output file names
getFullStats(metadata) {
let results = [];
let isImageAnimated = this.isAnimated(metadata) && Array.isArray(this.options.formatFiltering) && this.options.formatFiltering.includes("animated");
let hasAlpha = metadata.hasAlpha && Array.isArray(this.options.formatFiltering) && this.options.formatFiltering.includes("transparent");
let entryFormat = this.getEntryFormat(metadata);
let outputFormats = Image.getFormatsArray(this.options.formats, entryFormat, this.options.svgShortCircuit, isImageAnimated, hasAlpha);
if (this.needsRotation(metadata.orientation)) {
[metadata.height, metadata.width] = [metadata.width, metadata.height];
}
if(metadata.pageHeight) {
// When the { animated: true } option is provided to sharp, animated
// image formats like gifs or webp will have an inaccurate `height` value
// in their metadata which is actually the height of every single frame added together.
// In these cases, the metadata will contain an additional `pageHeight` property which
// is the height that the image should be displayed at.
metadata.height = metadata.pageHeight;
}
for(let outputFormat of outputFormats) {
if(!outputFormat || outputFormat === "auto") {
throw new Error("When using statsSync or statsByDimensionsSync, `formats: [null | 'auto']` to use the native image format is not supported.");
}
if(outputFormat === "svg") {
if(entryFormat === "svg") {
let svgStats = this.getStat("svg", metadata.width, metadata.height);
// SVG metadata.size is only available with Buffer input (remote urls)
if(metadata.size) {
// Note this is unfair for comparison with raster formats because its uncompressed (no GZIP, etc)
svgStats.size = metadata.size;
}
results.push(svgStats);
if(this.options.svgShortCircuit === true) {
break;
} else {
continue;
}
} else {
debug("Skipping SVG output for %o: received raster input.", this.src);
continue;
}
} else { // not outputting SVG (might still be SVG input though!)
let widths = Image.getValidWidths(metadata.width, this.options.widths, metadata.format === "svg" && this.options.svgAllowUpscale, this.options.minimumThreshold);
for(let width of widths) {
let height = Image.getAspectRatioHeight(metadata, width);
results.push(this.getStat(outputFormat, width, height));
}
}
}
return this.#transformRawFiles(results);
}
static getDimensionsFromSharp(sharpInstance, stat) {
let dims = {};
if(sharpInstance.options.width > -1) {
dims.width = sharpInstance.options.width;
dims.resized = true;
}
if(sharpInstance.options.height > -1) {
dims.height = sharpInstance.options.height;
dims.resized = true;
}
if(dims.width || dims.height) {
if(!dims.width) {
dims.width = Image.getAspectRatioWidth(stat, dims.height);
}
if(!dims.height) {
dims.height = Image.getAspectRatioHeight(stat, dims.width);
}
}
return dims;
}
static getAspectRatioWidth(originalDimensions, newHeight) {
return Math.floor(newHeight * originalDimensions.width / originalDimensions.height);
}
static getAspectRatioHeight(originalDimensions, newWidth) {
// Warning: if this is a guess via statsByDimensionsSync and that guess is wrong
// The aspect ratio will be wrong and any height/widths returned will be wrong!
return Math.floor(newWidth * originalDimensions.height / originalDimensions.width);
}
getOutputSize(contents, filePath) {
if(contents) {
if(this.options.svgCompressionSize === "br") {
return brotliSize(contents);
}
if("length" in contents) {
return contents.length;
}
}
// fallback to looking on local file system
if(!filePath) {
throw new Error("`filePath` expected.");
}
return fs.statSync(filePath).size;
}
isOutputCached(targetFile, sourceInput) {
if(!this.options.useCache) {
return false;
}
// last cache was a miss, so we must write to disk
if(this.assetCache && !this.assetCache.wasLastFetchCacheHit()) {
return false;
}
if(!diskCache.isCached(targetFile, sourceInput, !Util.isRequested(this.options.generatedVia))) {
return false;
}
return true;
}
// src should be a file path to an image or a buffer
async resize(input) {
let sharpInputImage = sharp(input, Object.assign({
// Deprecated by sharp, use `failOn` option instead
// https://github.com/lovell/sharp/blob/1533bf995acda779313fc178d2b9d46791349961/lib/index.d.ts#L915
failOnError: false,
}, this.options.sharpOptions));
// Must find the image format from the metadata
// File extensions lie or may not be present in the src url!
let sharpMetadata = await sharpInputImage.metadata();
let outputFilePromises = [];
let fullStats = this.getFullStats(sharpMetadata);
for(let outputFormat in fullStats) {
for(let stat of fullStats[outputFormat]) {
if(this.isOutputCached(stat.outputPath, input)) {
// Cached images already exist in output
let outputFileContents;
if(this.options.dryRun || outputFormat === "svg" && this.options.svgCompressionSize === "br") {
outputFileContents = this.getFileContents(stat.outputPath);
}
if(this.options.dryRun) {
stat.buffer = outputFileContents;
}
stat.size = this.getOutputSize(outputFileContents, stat.outputPath);
outputFilePromises.push(Promise.resolve(stat));
continue;
}
let sharpInstance = sharpInputImage.clone();
let transform = this.options.transform;
let isTransformResize = false;
if(transform) {
if(typeof transform !== "function") {
throw new Error("Expected `function` type in `transform` option. Received: " + transform);
}
await transform(sharpInstance);
// Resized in a transform (maybe for a crop)
let dims = Image.getDimensionsFromSharp(sharpInstance, stat);
if(dims.resized) {
isTransformResize = true;
// Overwrite current `stat` object with new sizes and file names
stat = this.getStat(stat.format, dims.width, dims.height);
}
}
// https://github.com/11ty/eleventy-img/issues/244
sharpInstance.keepIccProfile();
// Output images do not include orientation metadata (https://github.com/11ty/eleventy-img/issues/52)
// Use sharp.rotate to bake orientation into the image (https://github.com/lovell/sharp/blob/v0.32.6/docs/api-operation.md#rotate):
// > If no angle is provided, it is determined from the EXIF data. Mirroring is supported and may infer the use of a flip operation.
// > The use of rotate without an angle will remove the EXIF Orientation tag, if any.
if(this.options.fixOrientation || this.needsRotation(sharpMetadata.orientation)) {
sharpInstance.rotate();
}
if(!isTransformResize) {
if(stat.width < sharpMetadata.width || (this.options.svgAllowUpscale && sharpMetadata.format === "svg")) {
let resizeOptions = {
width: stat.width
};
if(sharpMetadata.format !== "svg" || !this.options.svgAllowUpscale) {
resizeOptions.withoutEnlargement = true;
}
sharpInstance.resize(resizeOptions);
}
}
// Format hooks take priority over Sharp processing.
// format hooks are only used for SVG out of the box
if(this.options.formatHooks && this.options.formatHooks[outputFormat]) {
let hookResult = await this.options.formatHooks[outputFormat].call(stat, sharpInstance);
if(hookResult) {
stat.size = this.getOutputSize(hookResult);
if(this.options.dryRun) {
stat.buffer = Buffer.from(hookResult);
outputFilePromises.push(Promise.resolve(stat));
} else {
this.directoryManager.createFromFile(stat.outputPath);
debugAssets("[11ty/eleventy-img] Writing %o", stat.outputPath);
outputFilePromises.push(fsp.writeFile(stat.outputPath, hookResult).then(() => stat));
}
}
} else { // not a format hook
let sharpFormatOptions = this.getSharpOptionsForFormat(outputFormat);
let hasFormatOptions = Object.keys(sharpFormatOptions).length > 0;
if(hasFormatOptions || outputFormat && sharpMetadata.format !== outputFormat) {
// https://github.com/lovell/sharp/issues/3680
// Fix heic regression in sharp 0.33
if(outputFormat === "heic" && !sharpFormatOptions.compression) {
sharpFormatOptions.compression = "av1";
}
sharpInstance.toFormat(outputFormat, sharpFormatOptions);
}
if(!this.options.dryRun && stat.outputPath) {
// Should never write when dryRun is true
this.directoryManager.createFromFile(stat.outputPath);
debugAssets("[11ty/eleventy-img] Writing %o", stat.outputPath);
outputFilePromises.push(
sharpInstance.toFile(stat.outputPath)
.then(info => {
stat.size = info.size;
return stat;
})
);
} else {
outputFilePromises.push(sharpInstance.toBuffer({ resolveWithObject: true }).then(({ data, info }) => {
stat.buffer = data;
stat.size = info.size;
return stat;
}));
}
}
if(stat.outputPath) {
if(this.options.dryRun) {
debug( "Generated %o", stat.url );
} else {
debug( "Wrote %o", stat.outputPath );
}
}
}
}
return Promise.all(outputFilePromises).then(files => this.#finalizeResults(this.#transformRawFiles(files)));
}
async getStatsOnly() {
if(typeof this.src !== "string" || !this.options.statsOnly) {
return;
}
let input;
if(Util.isRemoteUrl(this.src)) {
if(this.rawOptions.remoteImageMetadata?.width && this.rawOptions.remoteImageMetadata?.height) {
return this.getFullStats({
width: this.rawOptions.remoteImageMetadata.width,
height: this.rawOptions.remoteImageMetadata.height,
format: this.rawOptions.remoteImageMetadata.format, // only required if you want to use the "auto" format
guess: true,
});
}
// Fetch remote image to operate on it
// `remoteImageMetadata` is no longer required for statsOnly on remote images
input = await this.getInput();
}
// Local images
try {
// Related to https://github.com/11ty/eleventy-img/issues/295
let { width, height, type } = getImageSize(input || this.src);
return this.getFullStats({
width,
height,
format: type // only required if you want to use the "auto" format
});
} catch(e) {
throw new Error(`Eleventy Image error (statsOnly): \`image-size\` on "${this.src}" failed. Original error: ${e.message}`);
}
}
// returns raw Promise
queue() {
if(!this.#queue) {
return Promise.reject(new Error("Missing #queue."));
}
if(this.#queuePromise) {
return this.#queuePromise;
}
debug("Processing %o (in-memory cache miss), options: %o", this.src, this.options);
this.#queuePromise = this.#queue.add(async () => {
try {
if(typeof this.src === "string" && this.options.statsOnly) {
return this.getStatsOnly();
}
this.buildLogger.log(`Processing ${this.buildLogger.getFriendlyImageSource(this.src)}`, this.options);
let input = await this.getInput();
return this.resize(input);
} catch(e) {
this.buildLogger.error(`Error: ${e.message} (via ${this.buildLogger.getFriendlyImageSource(this.src)})`, this.options);
if(this.options.failOnError) {
throw e;
}
}
});
return this.#queuePromise;
}
// Factory to return from cache if available
static create(src, options = {}) {
let img = new Image(src, options);
// use resolved options for this
if(!img.options.useCache) {
return img;
}
let key = img.getInMemoryCacheKey();
let cached = memCache.get(key, !options.transformOnRequest && !Util.isRequested(options.generatedVia));
if(cached) {
return cached;
}
memCache.add(key, img);
return img;
}
/* `statsSync` doesnt generate any files, but will tell you where
* the asynchronously generated files will end up! This is useful
* in synchronous-only template environments where you need the
* image URLs synchronously but cant rely on the files being in
* the correct location yet.
*
* `options.dryRun` is still asynchronous but also doesnt generate
* any files.
*/
statsSync() {
if(this.isRemoteUrl) {
throw new Error("`statsSync` is not supported with remote sources. Use `statsByDimensionsSync(src, width, height, options)` instead.");
}
let dimensions = getImageSize(this.src);
return this.getFullStats({
width: dimensions.width,
height: dimensions.height,
format: dimensions.type,
});
}
static statsSync(src, opts) {
if(typeof src === "string" && Util.isRemoteUrl(src)) {
throw new Error("`statsSync` is not supported with remote sources. Use `statsByDimensionsSync(src, width, height, options)` instead.");
}
let img = Image.create(src, opts);
return img.statsSync();
}
statsByDimensionsSync(width, height) {
let dimensions = {
width,
height,
guess: true
};
return this.getFullStats(dimensions);
}
static statsByDimensionsSync(src, width, height, opts) {
let img = Image.create(src, opts);
return img.statsByDimensionsSync(width, height);
}
}
module.exports = Image;

54
node_modules/@11ty/eleventy-img/src/memory-cache.js generated vendored Normal file
View File

@ -0,0 +1,54 @@
const debug = require("debug")("Eleventy:Image");
class MemoryCache {
constructor() {
this.cache = {};
this.hitCounter = 0;
this.missCounter = 0;
}
resetCount() {
this.hitCounter = 0;
this.missCounter = 0;
}
getCount() {
return [this.hitCounter, this.missCounter];
}
add(key, results) {
this.cache[key] = {
results
};
debug("Unique images processed: %o", this.size());
}
get(key, incrementCounts = false) {
if(this.cache[key]) {
if(incrementCounts) {
this.hitCounter++;
}
// debug("Images re-used (via in-memory cache): %o", this.hitCounter);
// may return promise
return this.cache[key].results;
}
if(incrementCounts) {
this.missCounter++;
}
return false;
}
has(key) {
return key in this.cache;
}
size() {
return Object.keys(this.cache).length;
}
}
module.exports = MemoryCache;

View File

@ -0,0 +1,105 @@
const fs = require("node:fs");
const { TemplatePath } = require("@11ty/eleventy-utils");
const eleventyImage = require("../img.js");
const setupLogger = eleventyImage.setupLogger;
const Util = require("./util.js");
const debug = require("debug")("Eleventy:Image");
function eleventyImageOnRequestDuringServePlugin(eleventyConfig, options = {}) {
try {
// Throw an error if the application is not using Eleventy 3.0.0-alpha.7 or newer (including prereleases).
eleventyConfig.versionCheck(">=3.0.0-alpha.7");
} catch(e) {
console.log( `[11ty/eleventy-img] Warning: your version of Eleventy is incompatible with the dynamic image rendering plugin (see \`eleventyImageOnRequestDuringServePlugin\`). Any dynamically rendered images will 404 (be missing) during --serve mode but will not affect the standard build output: ${e.message}` );
}
setupLogger(eleventyConfig, {});
// Eleventy 3.0 or newer only.
eleventyConfig.setServerOptions({
onRequest: {
// TODO work with dev-servers option for `injectedScriptsFolder`
"/.11ty/image/": async function({ url }) {
// src could be file path or full url
let src = url.searchParams.get("src");
let imageFormat = url.searchParams.get("format");
let width = parseInt(url.searchParams.get("width"), 10);
let via = url.searchParams.get("via");
let defaultOptions;
if(via === "webc") {
defaultOptions = eleventyConfig.getFilter("__private_eleventyImageConfigurationOptions")();
} else if(via === "transform") {
defaultOptions = eleventyConfig.getFilter("__private_eleventyImageTransformConfigurationOptions")();
}
// if using this plugin directly (not via webc or transform), global default options will need to be passed in to the `addPlugin` call directly
// Prefer options passed to this plugin, fallback to Transform plugin or WebC options if the image source was generated via those options.
let opts = Object.assign({}, defaultOptions, options, {
widths: [width || "auto"],
formats: [imageFormat || "auto"],
dryRun: true,
cacheOptions: {
// We *do* want to write files to .cache for re-use here.
dryRun: false
},
transformOnRequest: false, // use the built images so we dont go in a loop
generatedVia: Util.KEYS.requested,
});
Util.addConfig(eleventyConfig, opts);
debug( `%o transformed on request to %o at %o width.`, src, imageFormat, width );
try {
if(!Util.isFullUrl(src)) {
// Image path on file system must be in working directory
src = TemplatePath.absolutePath(".", src);
if(!fs.existsSync(src) || !src.startsWith(TemplatePath.absolutePath("."))) {
throw new Error(`Invalid path: ${src}`);
}
}
let stats = await eleventyImage(src, opts);
let format = Object.keys(stats).pop();
let stat = stats[format][0];
if(!stat) {
throw new Error("Invalid image format.");
}
if(!stat.buffer) {
throw new Error("Could not find `buffer` property for image.");
}
return {
headers: {
// TODO Set cache headers to match eleventy-fetch cache options (though remote fetchs are still written to .cache)
"Content-Type": stat.sourceType,
},
body: stat.buffer,
};
} catch (error) {
debug("Error attempting to transform %o: %O", src, error);
return {
status: 500,
headers: {
"Content-Type": "image/svg+xml",
"x-error-message": error.message
},
body: `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="${width}" height="${width}" x="0" y="0" viewBox="0 0 1569.4 2186" xml:space="preserve" aria-hidden="true" focusable="false"><style>.st0{fill:#bbb;stroke:#bbb;stroke-width:28;stroke-miterlimit:10}</style><g><path class="st0" d="M562.2 1410.1c-9 0-13.5-12-13.5-36.1V778.9c0-11.5-2.3-16.9-7-16.2-28.4 7.2-42.7 10.8-43.1 10.8-7.9.7-11.8-7.2-11.8-23.7v-51.7c0-14.3 4.3-22.4 12.9-24.2l142.2-36.6c1.1-.3 2.7-.5 4.8-.5 7.9 0 11.8 8.4 11.8 25.3v712c0 24.1-4.7 36.1-14 36.1l-82.3-.1zM930.5 1411.2c-14.4 0-26.8-1-37.4-3-10.6-2-21.6-6.5-33.1-13.5s-20.9-16.6-28.3-28.8-13.4-29.3-18-51.2-7-47.9-7-78.1V960.4c0-7.2-2-10.8-5.9-10.8h-33.4c-9 0-13.5-8.6-13.5-25.8v-29.1c0-17.6 4.5-26.4 13.5-26.4h33.4c3.9 0 5.9-4.8 5.9-14.5l9.7-209.5c1.1-19 5.7-28.5 14-28.5h53.9c9 0 13.5 9.5 13.5 28.5v209.5c0 9.7 2.1 14.5 6.5 14.5H973c9 0 13.5 8.8 13.5 26.4v29.1c0 17.2-4.5 25.8-13.5 25.8h-68.9c-2.5 0-4.2.6-5.1 1.9-.9 1.2-1.3 4.2-1.3 8.9v277.9c0 20.8 1.3 38.2 4 52s6.6 24 11.8 30.4 10.4 10.8 15.6 12.9c5.2 2.2 11.6 3.2 19.1 3.2h38.2c9.7 0 14.5 6.7 14.5 19.9v32.3c0 14.7-5.2 22.1-15.6 22.1l-54.8.1zM1137.2 1475.8c8.2 0 15.4-6.7 21.5-20.2s9.2-32.6 9.2-57.4c0-5.8-3.6-25.7-10.8-59.8l-105.6-438.9c-.7-5-1.1-9-1.1-11.9 0-12.9 2.7-19.4 8.1-19.4h65.2c5 0 9.1 1.7 12.4 5.1s5.8 10.3 7.5 20.7l70 370.5c1.4 4.3 2.3 6.5 2.7 6.5 1.4 0 2.2-2 2.2-5.9l54.9-369.5c1.4-10.8 3.7-18 6.7-21.8s6.9-5.7 11.6-5.7h45.2c6.1 0 9.2 7 9.2 21 0 3.2-.4 7.4-1.1 12.4l-95.9 499.3c-7.5 41.3-15.8 72.9-24.8 94.8s-19 36.8-30.2 44.7c-11.1 7.9-25.8 12-44.2 12.4h-5.4c-29.1 0-48.8-7.7-59.2-23.2-2.9-3.2-4.3-11.5-4.3-24.8 0-26.6 4.3-39.9 12.9-39.9.7 0 7.2 1.8 19.4 5.4 12.4 3.8 20.3 5.6 23.9 5.6z"/><g><path class="st0" d="M291.2 1411.1c-9 0-13.5-12-13.5-36.1V779.9c0-11.5-2.3-16.9-7-16.2-28.4 7.2-42.7 10.8-43.1 10.8-7.9.7-11.8-7.2-11.8-23.7v-51.7c0-14.3 4.3-22.4 12.9-24.2L371 638.2c1.1-.3 2.7-.5 4.8-.5 7.9 0 11.8 8.4 11.8 25.3v712c0 24.1-4.7 36.1-14 36.1h-82.4z"/></g></g></svg>`,
};
}
}
}
});
}
module.exports = {
eleventyImageOnRequestDuringServePlugin,
};

210
node_modules/@11ty/eleventy-img/src/transform-plugin.js generated vendored Normal file
View File

@ -0,0 +1,210 @@
const path = require("node:path");
const Util = require("./util.js");
const { imageAttributesToPosthtmlNode, getOutputDirectory, cleanTag, isIgnored, isOptional } = require("./image-attrs-to-posthtml-node.js");
const { getGlobalOptions } = require("./global-options.js");
const { eleventyImageOnRequestDuringServePlugin } = require("./on-request-during-serve-plugin.js");
const PLACEHOLDER_DATA_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=";
const ATTRS = {
ORIGINAL_SOURCE: "eleventy:internal_original_src",
};
function getSrcAttributeValue(sourceNode/*, rootTargetNode*/) {
// Debatable TODO: use rootTargetNode (if `picture`) to retrieve a potentially higher quality source from <source srcset>
return sourceNode.attrs?.src;
}
function assignAttributes(rootTargetNode, newNode) {
// only copy attributes if old and new tag name are the same (picture => picture, img => img)
if(rootTargetNode.tag !== newNode.tag) {
delete rootTargetNode.attrs;
}
if(!rootTargetNode.attrs) {
rootTargetNode.attrs = {};
}
// Copy all new attributes to target
if(newNode.attrs) {
Object.assign(rootTargetNode.attrs, newNode.attrs);
}
}
function getOutputLocations(originalSource, outputDirectoryFromAttribute, pageContext, options) {
let projectOutputDirectory = options.directories.output;
if(outputDirectoryFromAttribute) {
if(path.isAbsolute(outputDirectoryFromAttribute)) {
return {
outputDir: path.join(projectOutputDirectory, outputDirectoryFromAttribute),
urlPath: outputDirectoryFromAttribute,
};
}
return {
outputDir: path.join(projectOutputDirectory, pageContext.url, outputDirectoryFromAttribute),
urlPath: path.join(pageContext.url, outputDirectoryFromAttribute),
};
}
if(options.urlPath) {
// do nothing, user has specified directories in the plugin options.
return {};
}
if(path.isAbsolute(originalSource)) {
// if the path is an absolute one (relative to the content directory) write to a global output directory to avoid duplicate writes for identical source images.
return {
outputDir: path.join(projectOutputDirectory, "/img/"),
urlPath: "/img/",
};
}
// If original source is a relative one, this colocates images to the template output.
let dir = path.dirname(pageContext.outputPath);
// filename is included in url: ./dir/post.html => /dir/post.html
if(pageContext.outputPath.endsWith(pageContext.url)) {
// remove file name
let split = pageContext.url.split("/");
split[split.length - 1] = "";
return {
outputDir: dir,
urlPath: split.join("/"),
};
}
// filename is not included in url: ./dir/post/index.html => /dir/post/
return {
outputDir: dir,
urlPath: pageContext.url,
};
}
function transformTag(context, sourceNode, rootTargetNode, opts) {
let originalSource = getSrcAttributeValue(sourceNode, rootTargetNode);
if(!originalSource) {
return sourceNode;
}
let { inputPath } = context.page;
sourceNode.attrs.src = Util.normalizeImageSource({
input: opts.directories.input,
inputPath,
}, originalSource, {
isViaHtml: true, // this reference came from HTML, so we can decode the file name
});
if(sourceNode.attrs.src !== originalSource) {
sourceNode.attrs[ATTRS.ORIGINAL_SOURCE] = originalSource;
}
let outputDirectoryFromAttribute = getOutputDirectory(sourceNode);
let instanceOptions = getOutputLocations(originalSource, outputDirectoryFromAttribute, context.page, opts);
// returns promise
return imageAttributesToPosthtmlNode(sourceNode.attrs, instanceOptions, opts).then(newNode => {
// node.tag
// node.attrs
// node.content
assignAttributes(rootTargetNode, newNode);
rootTargetNode.tag = newNode.tag;
rootTargetNode.content = newNode.content;
}, (error) => {
if(isOptional(sourceNode) || !opts.failOnError) {
if(isOptional(sourceNode, "keep")) {
// replace with the original source value, no image transformation is taking place
if(sourceNode.attrs[ATTRS.ORIGINAL_SOURCE]) {
sourceNode.attrs.src = sourceNode.attrs[ATTRS.ORIGINAL_SOURCE];
}
// leave as-is, likely 404 when a user visits the page
} else if(isOptional(sourceNode, "placeholder")) {
// transparent png
sourceNode.attrs.src = PLACEHOLDER_DATA_URI;
} else if(isOptional(sourceNode)) {
delete sourceNode.attrs.src;
}
// optional or dont fail on error
cleanTag(sourceNode);
return Promise.resolve();
}
return Promise.reject(error);
});
}
function eleventyImageTransformPlugin(eleventyConfig, options = {}) {
options = Object.assign({
extensions: "html",
transformOnRequest: process.env.ELEVENTY_RUN_MODE === "serve",
}, options);
if(options.transformOnRequest !== false) {
// Add the on-request plugin automatically (unless opt-out in this plugins options only)
eleventyConfig.addPlugin(eleventyImageOnRequestDuringServePlugin);
}
// Notably, global options are not shared automatically with the WebC `eleventyImagePlugin` above.
// Devs can pass in the same object to both if they want!
let opts = getGlobalOptions(eleventyConfig, options, "transform");
eleventyConfig.addJavaScriptFunction("__private_eleventyImageTransformConfigurationOptions", () => {
return opts;
});
function posthtmlPlugin(context) {
return async (tree) => {
let promises = [];
let match = tree.match;
tree.match({ tag: 'picture' }, pictureNode => {
match.call(pictureNode, { tag: 'img' }, imgNode => {
imgNode._insideOfPicture = true;
if(!isIgnored(imgNode) && !imgNode?.attrs?.src?.startsWith("data:")) {
promises.push(transformTag(context, imgNode, pictureNode, opts));
}
return imgNode;
});
return pictureNode;
});
tree.match({ tag: 'img' }, (imgNode) => {
if(imgNode._insideOfPicture) {
delete imgNode._insideOfPicture;
} else if(isIgnored(imgNode) || imgNode?.attrs?.src?.startsWith("data:")) {
cleanTag(imgNode);
} else {
promises.push(transformTag(context, imgNode, imgNode, opts));
}
return imgNode;
});
await Promise.all(promises);
return tree;
};
}
if(!eleventyConfig.htmlTransformer || !("addPosthtmlPlugin" in eleventyConfig.htmlTransformer)) {
throw new Error("[@11ty/eleventy-img] `eleventyImageTransformPlugin` is not compatible with this version of Eleventy. You will need to use v3.0.0 or newer.");
}
eleventyConfig.htmlTransformer.addPosthtmlPlugin(options.extensions, posthtmlPlugin, {
priority: -1, // we want this to go before <base> or inputpath to url
});
}
module.exports = {
eleventyImageTransformPlugin,
};

78
node_modules/@11ty/eleventy-img/src/util.js generated vendored Normal file
View File

@ -0,0 +1,78 @@
const path = require("node:path");
class Util {
static KEYS = {
requested: "requested"
};
/*
* Does not mutate, returns new Object.
*/
static getSortedObject(unordered) {
let keys = Object.keys(unordered).sort();
let obj = {};
for(let key of keys) {
obj[key] = unordered[key];
}
return obj;
}
static isRemoteUrl(url) {
try {
const validUrl = new URL(url);
if (validUrl.protocol.startsWith("https:") || validUrl.protocol.startsWith("http:")) {
return true;
}
return false;
// eslint-disable-next-line no-unused-vars
} catch(e) {
// invalid url OR local path
return false;
}
}
static normalizeImageSource({ input, inputPath }, src, options = {}) {
let { isViaHtml } = Object.assign({
isViaHtml: false
}, options);
if(Util.isFullUrl(src)) {
return src;
}
if(isViaHtml) {
src = decodeURIComponent(src);
}
if(!path.isAbsolute(src)) {
// if the image src is relative, make it relative to the template file (inputPath);
let dir = path.dirname(inputPath);
return path.join(dir, src);
}
// if the image src is absolute, make it relative to the input/content directory.
return path.join(input, src);
}
static isRequested(generatedVia) {
return generatedVia === this.KEYS.requested;
}
static addConfig(eleventyConfig, options) {
if(!eleventyConfig) {
return;
}
Object.defineProperty(options, "eleventyConfig", {
value: eleventyConfig,
enumerable: false,
});
}
}
// Temporary alias for changes made in https://github.com/11ty/eleventy-img/pull/138
Util.isFullUrl = Util.isRemoteUrl;
module.exports = Util;

View File

@ -0,0 +1,23 @@
const { getGlobalOptions } = require("./global-options.js");
const { eleventyImageOnRequestDuringServePlugin } = require("./on-request-during-serve-plugin.js");
function eleventyWebcOptionsPlugin(eleventyConfig, options = {}) {
options = Object.assign({
transformOnRequest: process.env.ELEVENTY_RUN_MODE === "serve",
}, options);
// Notably, global options are not shared automatically with the `eleventyImageTransformPlugin` below.
// Devs can pass in the same object to both if they want!
eleventyConfig.addJavaScriptFunction("__private_eleventyImageConfigurationOptions", () => {
return getGlobalOptions(eleventyConfig, options, "webc");
});
if(options.transformOnRequest !== false) {
// Add the on-request plugin automatically (unless opt-out in this plugins options only)
eleventyConfig.addPlugin(eleventyImageOnRequestDuringServePlugin);
}
}
module.exports = {
eleventyWebcOptionsPlugin,
};