211 lines
6.8 KiB
JavaScript
211 lines
6.8 KiB
JavaScript
|
|
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 don’t 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,
|
|||
|
|
};
|