Skip to content

Commit

Permalink
Fixes #208.
Browse files Browse the repository at this point in the history
  • Loading branch information
zachleat committed Feb 10, 2024
1 parent d032476 commit bd7a815
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 8 deletions.
1 change: 1 addition & 0 deletions eleventy-image.webc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<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>");
Expand Down
39 changes: 32 additions & 7 deletions img.js
Original file line number Diff line number Diff line change
Expand Up @@ -791,20 +791,45 @@ const generateHTML = require("./src/generate-html.js");
module.exports.generateHTML = generateHTML;
module.exports.generateObject = generateHTML.generateObject;

function getGlobalOptions(eleventyDirectories, options) {
return Object.assign({
packages: {
image: module.exports,
},
outputDir: path.join(eleventyDirectories.output, options.urlPath || ""),
}, options);
}

module.exports.eleventyImagePlugin = function(eleventyConfig, options = {}) {
let eleventyDirectories;
eleventyConfig.on("eleventy.directories", (dirs) => {
eleventyDirectories = dirs;
});

// 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 Object.assign({
packages: {
image: module.exports,
},
outputDir: path.join(eleventyDirectories.output, options.urlPath || ""),
}, options);
return getGlobalOptions(eleventyDirectories, options);
});
};

const transformPlugin = require("./src/transformPlugin.js");
module.exports.eleventyImageTransformPlugin = function(eleventyConfig, options = {}) {
options = Object.assign({
extensions: "html",
}, options);

let eleventyDirectories;
eleventyConfig.on("eleventy.directories", (dirs) => {
eleventyDirectories = dirs;
});

// TODO expose the `imagePlugin` in eleventy-image.webc for re-use in a provided shortcode.
// Notably, global options are not shared automatically with the WebC `eleventyImagePlugin` above.
// Devs can pass in the same object to both if they want!
transformPlugin(eleventyConfig, options, () => {
let opts = getGlobalOptions(eleventyDirectories, options);
opts.eleventyDirectories = eleventyDirectories;
delete opts.packages;
return opts;
});
};
2 changes: 1 addition & 1 deletion src/generate-html.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ function mapObjectToHTML(tagName, attrs = {}) {
function generateHTML(metadata, attributes = {}, options = {}) {
let isInline = options.whitespaceMode !== "block";
let markup = [];
let obj = generateObject(metadata, attributes, options);
let obj = generateObject(metadata, attributes);
for(let tag in obj) {
if(!Array.isArray(obj[tag])) {
markup.push(mapObjectToHTML(tag, obj[tag]));
Expand Down
95 changes: 95 additions & 0 deletions src/imageAttributesToPosthtmlNode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
const eleventyImage = require("../");

const ATTR_PREFIX = "eleventy:";

const ATTR = {
IGNORE: `${ATTR_PREFIX}ignore`,
WIDTHS: `${ATTR_PREFIX}widths`,
FORMATS: `${ATTR_PREFIX}formats`,
OUTPUT: `${ATTR_PREFIX}output`,
};

function convertToPosthtmlNode(obj) {
// node.tag
// node.attrs
// node.content

let node = {};
let [key] = Object.keys(obj);
node.tag = key;

if(Array.isArray(obj[key])) {
node.content = obj[key].map(child => {
return convertToPosthtmlNode(child);
});
} else {
node.attrs = obj[key];
}

return node;
}

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?")
}

let defaultGlobalAttributes = globalPluginOptions.defaultAttributes;
delete globalPluginOptions.defaultAttributes;

if(!instanceOptions) {
instanceOptions = {};
}

// overrides global widths
if(attributes[ATTR.WIDTHS]) {
if(typeof attributes[ATTR.WIDTHS] === "string") {
instanceOptions.widths = attributes[ATTR.WIDTHS].split(",").map(entry => parseInt(entry, 10));
delete attributes[ATTR.WIDTHS];
}
}

if(attributes[ATTR.FORMATS]) {
if(typeof attributes[ATTR.FORMATS] === "string") {
instanceOptions.formats = attributes[ATTR.FORMATS].split(",");
delete attributes[ATTR.FORMATS];
}
}

let options = Object.assign({}, globalPluginOptions, instanceOptions);
let metadata = await eleventyImage(attributes.src, options);
let imageAttributes = Object.assign({}, defaultGlobalAttributes, attributes);

// You bet we throw an error on missing alt in `imageAttributes` (alt="" works okay)
let obj = await eleventyImage.generateObject(metadata, imageAttributes);
return convertToPosthtmlNode(obj);
};

function cleanTag(node) {
// Delete all prefixed attributes
for(let key in node?.attrs) {
if(key.startsWith(ATTR_PREFIX)) {
delete node?.attrs?.[key];
}
}
}

function isIgnored(node) {
return node?.attrs && node?.attrs?.[ATTR.IGNORE] !== undefined;
}

function getOutputDirectory(node) {
return node?.attrs?.[ATTR.OUTPUT];
}

module.exports = {
imageAttributesToPosthtmlNode,
cleanTag,
isIgnored,
getOutputDirectory,
}
105 changes: 105 additions & 0 deletions src/transformPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
const path = require("path");
const { imageAttributesToPosthtmlNode, getOutputDirectory, cleanTag, isIgnored } = require("./imageAttributesToPosthtmlNode.js");

function isFullUrl(url) {
try {
new URL(url);
return true;
} catch(e) {
return false;
}
}

function normalizeImageSource({ inputPath, contentDir }, src) {
if(isFullUrl(src)) {
return 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 content directory.
return path.join(contentDir, src);
}

function transformTag(context, node, opts) {
let originalSource = node.attrs.src;
let { inputPath, outputPath, url } = context.page;

node.attrs.src = normalizeImageSource({
inputPath,
contentDir: opts.eleventyDirectories.input,
}, originalSource);

let instanceOptions = {};

let outputDirectory = getOutputDirectory(node);
if(outputDirectory) {
if(path.isAbsolute(outputDirectory)) {
instanceOptions = {
outputDir: path.join(opts.eleventyDirectories.output, outputDirectory),
urlPath: outputDirectory,
};
} else {
instanceOptions = {
outputDir: path.join(opts.eleventyDirectories.output, url, outputDirectory),
urlPath: path.join(url, outputDirectory),
};
}
} else if(opts.urlPath) {
// do nothing, user has specified directories in the plugin options.
} else 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.
instanceOptions = {
outputDir: path.join(opts.eleventyDirectories.output, "/img/"),
urlPath: "/img/",
};
} else {
// If original source is a relative one, this colocates images to the template output.
instanceOptions = {
outputDir: path.dirname(outputPath),
urlPath: url,
};
}

// returns promise
return imageAttributesToPosthtmlNode(node.attrs, instanceOptions, opts).then(obj => {
// TODO how to assign attributes to `<picture>` only
// Wipe out attrs just in case this is <picture>
node.attrs = {};

Object.assign(node, obj);
});
}

module.exports = function(eleventyConfig, options, globalOptionsCallback) {
function posthtmlPlugin(context) {
let opts = globalOptionsCallback();

return (tree) => {
let promises = [];
tree.match({ tag: 'img' }, (node) => {
if(isIgnored(node)) {
cleanTag(node);
} else {
promises.push(transformTag(context, node, opts));
}

return node;
});

return Promise.all(promises).then(() => 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
});
};

0 comments on commit bd7a815

Please sign in to comment.