Skip to content

Commit bd7a815

Browse files
committed
Fixes #208.
1 parent d032476 commit bd7a815

5 files changed

+234
-8
lines changed

eleventy-image.webc

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<script webc:type="js">
2323
const path = require("path");
2424

25+
// TODO expose this for re-use in a provided shortcode.
2526
async function imagePlugin(attributes, globalPluginOptions) {
2627
if(!attributes.src) {
2728
throw new Error("Missing `src` attribute on <eleventy-image>");

img.js

+32-7
Original file line numberDiff line numberDiff line change
@@ -791,20 +791,45 @@ const generateHTML = require("./src/generate-html.js");
791791
module.exports.generateHTML = generateHTML;
792792
module.exports.generateObject = generateHTML.generateObject;
793793

794+
function getGlobalOptions(eleventyDirectories, options) {
795+
return Object.assign({
796+
packages: {
797+
image: module.exports,
798+
},
799+
outputDir: path.join(eleventyDirectories.output, options.urlPath || ""),
800+
}, options);
801+
}
802+
794803
module.exports.eleventyImagePlugin = function(eleventyConfig, options = {}) {
795804
let eleventyDirectories;
796805
eleventyConfig.on("eleventy.directories", (dirs) => {
797806
eleventyDirectories = dirs;
798807
});
799808

809+
// Notably, global options are not shared automatically with the `eleventyImageTransformPlugin` below.
810+
// Devs can pass in the same object to both if they want!
800811
eleventyConfig.addJavaScriptFunction("__private_eleventyImageConfigurationOptions", () => {
801-
return Object.assign({
802-
packages: {
803-
image: module.exports,
804-
},
805-
outputDir: path.join(eleventyDirectories.output, options.urlPath || ""),
806-
}, options);
812+
return getGlobalOptions(eleventyDirectories, options);
813+
});
814+
};
815+
816+
const transformPlugin = require("./src/transformPlugin.js");
817+
module.exports.eleventyImageTransformPlugin = function(eleventyConfig, options = {}) {
818+
options = Object.assign({
819+
extensions: "html",
820+
}, options);
821+
822+
let eleventyDirectories;
823+
eleventyConfig.on("eleventy.directories", (dirs) => {
824+
eleventyDirectories = dirs;
807825
});
808826

809-
// TODO expose the `imagePlugin` in eleventy-image.webc for re-use in a provided shortcode.
827+
// Notably, global options are not shared automatically with the WebC `eleventyImagePlugin` above.
828+
// Devs can pass in the same object to both if they want!
829+
transformPlugin(eleventyConfig, options, () => {
830+
let opts = getGlobalOptions(eleventyDirectories, options);
831+
opts.eleventyDirectories = eleventyDirectories;
832+
delete opts.packages;
833+
return opts;
834+
});
810835
};

src/generate-html.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ function mapObjectToHTML(tagName, attrs = {}) {
168168
function generateHTML(metadata, attributes = {}, options = {}) {
169169
let isInline = options.whitespaceMode !== "block";
170170
let markup = [];
171-
let obj = generateObject(metadata, attributes, options);
171+
let obj = generateObject(metadata, attributes);
172172
for(let tag in obj) {
173173
if(!Array.isArray(obj[tag])) {
174174
markup.push(mapObjectToHTML(tag, obj[tag]));

src/imageAttributesToPosthtmlNode.js

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
const eleventyImage = require("../");
2+
3+
const ATTR_PREFIX = "eleventy:";
4+
5+
const ATTR = {
6+
IGNORE: `${ATTR_PREFIX}ignore`,
7+
WIDTHS: `${ATTR_PREFIX}widths`,
8+
FORMATS: `${ATTR_PREFIX}formats`,
9+
OUTPUT: `${ATTR_PREFIX}output`,
10+
};
11+
12+
function convertToPosthtmlNode(obj) {
13+
// node.tag
14+
// node.attrs
15+
// node.content
16+
17+
let node = {};
18+
let [key] = Object.keys(obj);
19+
node.tag = key;
20+
21+
if(Array.isArray(obj[key])) {
22+
node.content = obj[key].map(child => {
23+
return convertToPosthtmlNode(child);
24+
});
25+
} else {
26+
node.attrs = obj[key];
27+
}
28+
29+
return node;
30+
}
31+
32+
async function imageAttributesToPosthtmlNode(attributes, instanceOptions, globalPluginOptions) {
33+
34+
if(!attributes.src) {
35+
throw new Error("Missing `src` attribute for `@11ty/eleventy-img`");
36+
}
37+
38+
if(!globalPluginOptions) {
39+
throw new Error("Missing global defaults for `@11ty/eleventy-img`: did you call addPlugin?")
40+
}
41+
42+
let defaultGlobalAttributes = globalPluginOptions.defaultAttributes;
43+
delete globalPluginOptions.defaultAttributes;
44+
45+
if(!instanceOptions) {
46+
instanceOptions = {};
47+
}
48+
49+
// overrides global widths
50+
if(attributes[ATTR.WIDTHS]) {
51+
if(typeof attributes[ATTR.WIDTHS] === "string") {
52+
instanceOptions.widths = attributes[ATTR.WIDTHS].split(",").map(entry => parseInt(entry, 10));
53+
delete attributes[ATTR.WIDTHS];
54+
}
55+
}
56+
57+
if(attributes[ATTR.FORMATS]) {
58+
if(typeof attributes[ATTR.FORMATS] === "string") {
59+
instanceOptions.formats = attributes[ATTR.FORMATS].split(",");
60+
delete attributes[ATTR.FORMATS];
61+
}
62+
}
63+
64+
let options = Object.assign({}, globalPluginOptions, instanceOptions);
65+
let metadata = await eleventyImage(attributes.src, options);
66+
let imageAttributes = Object.assign({}, defaultGlobalAttributes, attributes);
67+
68+
// You bet we throw an error on missing alt in `imageAttributes` (alt="" works okay)
69+
let obj = await eleventyImage.generateObject(metadata, imageAttributes);
70+
return convertToPosthtmlNode(obj);
71+
};
72+
73+
function cleanTag(node) {
74+
// Delete all prefixed attributes
75+
for(let key in node?.attrs) {
76+
if(key.startsWith(ATTR_PREFIX)) {
77+
delete node?.attrs?.[key];
78+
}
79+
}
80+
}
81+
82+
function isIgnored(node) {
83+
return node?.attrs && node?.attrs?.[ATTR.IGNORE] !== undefined;
84+
}
85+
86+
function getOutputDirectory(node) {
87+
return node?.attrs?.[ATTR.OUTPUT];
88+
}
89+
90+
module.exports = {
91+
imageAttributesToPosthtmlNode,
92+
cleanTag,
93+
isIgnored,
94+
getOutputDirectory,
95+
}

src/transformPlugin.js

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
const path = require("path");
2+
const { imageAttributesToPosthtmlNode, getOutputDirectory, cleanTag, isIgnored } = require("./imageAttributesToPosthtmlNode.js");
3+
4+
function isFullUrl(url) {
5+
try {
6+
new URL(url);
7+
return true;
8+
} catch(e) {
9+
return false;
10+
}
11+
}
12+
13+
function normalizeImageSource({ inputPath, contentDir }, src) {
14+
if(isFullUrl(src)) {
15+
return src;
16+
}
17+
18+
if(!path.isAbsolute(src)) {
19+
// if the image src is relative, make it relative to the template file (inputPath);
20+
let dir = path.dirname(inputPath);
21+
return path.join(dir, src);
22+
}
23+
24+
// if the image src is absolute, make it relative to the content directory.
25+
return path.join(contentDir, src);
26+
}
27+
28+
function transformTag(context, node, opts) {
29+
let originalSource = node.attrs.src;
30+
let { inputPath, outputPath, url } = context.page;
31+
32+
node.attrs.src = normalizeImageSource({
33+
inputPath,
34+
contentDir: opts.eleventyDirectories.input,
35+
}, originalSource);
36+
37+
let instanceOptions = {};
38+
39+
let outputDirectory = getOutputDirectory(node);
40+
if(outputDirectory) {
41+
if(path.isAbsolute(outputDirectory)) {
42+
instanceOptions = {
43+
outputDir: path.join(opts.eleventyDirectories.output, outputDirectory),
44+
urlPath: outputDirectory,
45+
};
46+
} else {
47+
instanceOptions = {
48+
outputDir: path.join(opts.eleventyDirectories.output, url, outputDirectory),
49+
urlPath: path.join(url, outputDirectory),
50+
};
51+
}
52+
} else if(opts.urlPath) {
53+
// do nothing, user has specified directories in the plugin options.
54+
} else if(path.isAbsolute(originalSource)) {
55+
// 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.
56+
instanceOptions = {
57+
outputDir: path.join(opts.eleventyDirectories.output, "/img/"),
58+
urlPath: "/img/",
59+
};
60+
} else {
61+
// If original source is a relative one, this colocates images to the template output.
62+
instanceOptions = {
63+
outputDir: path.dirname(outputPath),
64+
urlPath: url,
65+
};
66+
}
67+
68+
// returns promise
69+
return imageAttributesToPosthtmlNode(node.attrs, instanceOptions, opts).then(obj => {
70+
// TODO how to assign attributes to `<picture>` only
71+
// Wipe out attrs just in case this is <picture>
72+
node.attrs = {};
73+
74+
Object.assign(node, obj);
75+
});
76+
}
77+
78+
module.exports = function(eleventyConfig, options, globalOptionsCallback) {
79+
function posthtmlPlugin(context) {
80+
let opts = globalOptionsCallback();
81+
82+
return (tree) => {
83+
let promises = [];
84+
tree.match({ tag: 'img' }, (node) => {
85+
if(isIgnored(node)) {
86+
cleanTag(node);
87+
} else {
88+
promises.push(transformTag(context, node, opts));
89+
}
90+
91+
return node;
92+
});
93+
94+
return Promise.all(promises).then(() => tree);
95+
};
96+
}
97+
98+
if(!eleventyConfig.htmlTransformer || !("addPosthtmlPlugin" in eleventyConfig.htmlTransformer)) {
99+
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.");
100+
}
101+
102+
eleventyConfig.htmlTransformer.addPosthtmlPlugin(options.extensions, posthtmlPlugin, {
103+
priority: -1, // we want this to go before <base> or inputpath to url
104+
});
105+
};

0 commit comments

Comments
 (0)