import path from "node:path"; import fs from "node:fs/promises"; import Image from "@11ty/eleventy-img"; // Some files in src/images have a literal query string in their filename (e.g. `foo.jpeg?width=300`) const IMAGE_EXTENSIONS = /\.(jpe?g|png|webp|gif|avif|svg|tiff?)(\?.*)?$/i; /** * Renders local markdown images as responsive elements via Eleventy Image. * * Replaces `markdown-it-eleventy-img`, which is unmaintained and pinned to an old * (vulnerable) Eleventy Image + sharp. Eleventy Image v7 dropped its synchronous * stats API, but markdown-it renders synchronously, so image metadata is read * up-front in `eleventy.before` and looked up while rendering. Images are only * generated once a page actually references them, and `eleventy.after` waits for * those to finish. * * Remote images are passed through to markdown-it's default renderer untouched. */ export function createMarkdownImages({ imageDir, imgOptions = {}, globalAttributes = {} }) { const metadataByPath = new Map(); let pendingGenerations = []; async function loadMetadata() { const entries = await fs.readdir(imageDir, { recursive: true, withFileTypes: true }); const files = entries .filter(entry => entry.isFile() && IMAGE_EXTENSIONS.test(entry.name)) .map(entry => path.join(entry.parentPath, entry.name)); await Promise.all(files.map(async file => { const metadata = await Image(file, { ...imgOptions, statsOnly: true }); metadataByPath.set(path.normalize(file), metadata); })); } async function waitForGenerations() { const generations = pendingGenerations; pendingGenerations = []; await Promise.all(generations); } function markdownItPlugin(md) { md.renderer.rules.image = (tokens, index, rendererOptions, env, renderer) => { const token = tokens[index]; const src = token.attrGet("src"); if (Image.Util.isRemoteUrl(src)) { token.attrSet("alt", token.content); return renderer.renderToken(tokens, index, rendererOptions); } const filePath = path.normalize(decodeURIComponent(src)); const metadata = metadataByPath.get(filePath); if (!metadata) { throw new Error(`Markdown image "${src}" was not found in ${imageDir}. Local markdown images must live in that directory.`); } pendingGenerations.push(Image(filePath, imgOptions)); const tokenAttributes = Object.fromEntries( token.attrs .filter(([name]) => name !== "src") .map(([name, value]) => [name.toLowerCase().trim(), value]) ); return Image.generateHTML(metadata, { ...globalAttributes, ...tokenAttributes, alt: token.content, }, { whitespaceMode: "inline" }); }; } function eleventyPlugin(eleventyConfig) { eleventyConfig.on("eleventy.before", loadMetadata); eleventyConfig.on("eleventy.after", waitForGenerations); } return { markdownItPlugin, eleventyPlugin }; }