├── .gitignore
├── .editorconfig
├── README.md
├── LICENSE
├── package.json
└── .eleventy.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | package-lock.json
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
9 | charset = utf-8
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # eleventy-plugin-img
2 |
3 | ⚠️⚠️⚠️ This plugin has been temporarily superceded by the lower level utility [`eleventy-img`](https://github.com/11ty/eleventy-img). Please use that instead!
4 |
5 | * https://github.com/11ty/eleventy-img
6 |
7 | ## Installation
8 |
9 | Available on [npm](https://www.npmjs.com/package/@11ty/eleventy-plugin-img).
10 |
11 | ```
12 | npm install @11ty/eleventy-plugin-img --save-dev
13 | ```
14 |
15 | Open up your Eleventy config file (probably `.eleventy.js`) and use `addPlugin`:
16 |
17 | ```
18 | const pluginImg = require("@11ty/eleventy-plugin-img");
19 | module.exports = function(eleventyConfig) {
20 | eleventyConfig.addPlugin(pluginImg);
21 | };
22 | ```
23 |
24 | Read more about [Eleventy plugins.](https://www.11ty.io/docs/plugins/)
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Zach Leatherman @zachleat
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@11ty/eleventy-plugin-img",
3 | "version": "1.0.0-beta.3",
4 | "description": "A plugin to perform runtime image transformations.",
5 | "publishConfig": {
6 | "access": "public"
7 | },
8 | "main": ".eleventy.js",
9 | "scripts": {
10 | "test": "echo \"Error: no test specified\" && exit 1"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/11ty/eleventy-plugin-img.git"
15 | },
16 | "funding": {
17 | "type": "opencollective",
18 | "url": "https://opencollective.com/11ty"
19 | },
20 | "keywords": [
21 | "eleventy",
22 | "eleventy-plugin"
23 | ],
24 | "author": {
25 | "name": "Zach Leatherman",
26 | "email": "zachleatherman@gmail.com",
27 | "url": "https://zachleat.com/"
28 | },
29 | "license": "MIT",
30 | "bugs": {
31 | "url": "https://github.com/11ty/eleventy-plugin-img/issues"
32 | },
33 | "homepage": "https://github.com/11ty/eleventy-plugin-img#readme",
34 | "peerDependencies": {
35 | "@11ty/eleventy": ">=0.10.0"
36 | },
37 | "dependencies": {
38 | "avatar-local-cache": "^2.0.6",
39 | "flat-cache": "^2.0.1",
40 | "short-hash": "^1.0.0"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/.eleventy.js:
--------------------------------------------------------------------------------
1 | // TODO
2 | //
via https://twitter.com/kornelski/status/238192634827505664
3 | // Avatar local cache has maximum size assumptions that need to be removed
4 |
5 | // TODO if 404 or other bad status code, write that to cache! to avoid re-requests
6 | const shorthash = require("short-hash");
7 | const path = require("path");
8 | const flatCache = require("flat-cache");
9 | const { URL } = require("url");
10 | const AvatarLocalCache = require("avatar-local-cache");
11 |
12 | const IMG_DIRECTORY = "img/";
13 | const CACHE_DIRECTORY = ".cache/";
14 | const OFFLINE_MODE = false;
15 |
16 | function serializeObjectToAttributes(obj = {}) {
17 | let ret = [];
18 | for( let attrName in obj ) {
19 | if(attrName === "__keywords") continue; // weird Nunjucks thing
20 | ret.push(` ${attrName}="${obj[attrName]}"`);
21 | }
22 | return ret.join("");
23 | }
24 |
25 | function isFullUrl(url) {
26 | try {
27 | new URL(url);
28 | return true;
29 | } catch(e) {
30 | // invalid url OR local path
31 | return false;
32 | }
33 | }
34 |
35 | function getFormatsArray(formats) {
36 | if(formats && formats.length) {
37 | if(typeof formats === "string") {
38 | formats = formats.split(",");
39 | }
40 | return formats;
41 | }
42 |
43 | return [];
44 | }
45 |
46 | function getDurationMs(duration = "0s") {
47 | let durationUnits = duration.substr(-1);
48 | let durationMultiplier;
49 | if(durationUnits === "s") {
50 | durationMultiplier = 1;
51 | } else if(durationUnits === "m") {
52 | durationMultiplier = 60;
53 | } else if(durationUnits === "h") {
54 | durationMultiplier = 60 * 60;
55 | } else if(durationUnits === "d") {
56 | durationMultiplier = 60 * 60 * 24;
57 | } else if(durationUnits === "w") {
58 | durationMultiplier = 60 * 60 * 24 * 7;
59 | } else if(durationUnits === "y") {
60 | durationMultiplier = 60 * 60 * 24 * 365;
61 | }
62 |
63 | let durationValue = parseInt(duration.substr(0, duration.length - 1), 10);
64 | return durationValue * durationMultiplier * 1000;
65 | }
66 |
67 | function imgShortcode(props = {}, options = {}) {
68 | options = Object.assign({
69 | cacheremotesrc: true,
70 | allowmissingalt: false,
71 | duration: '1d',
72 | imgDirectory: IMG_DIRECTORY,
73 | cacheDirectory: CACHE_DIRECTORY,
74 | offlineMode: OFFLINE_MODE,
75 | imgSrcDirectory: "",
76 | pathPrefix: "/",
77 | addWidthHeight: true,
78 | // image formats
79 | formats: null,
80 | // skipCache: false, TODO
81 | // maxwidth: 400 // in px
82 | }, options);
83 |
84 | if(!options.allowmissingalt && !("alt" in props)) {
85 | throw new Error(`Missing [alt] attribute on
`);
86 | }
87 |
88 | let url = props.src;
89 | let validUrl = isFullUrl(url);
90 |
91 | return new Promise(function(resolve) {
92 | if(!options.cacheremotesrc || !validUrl) {
93 | resolve([url]);
94 | return;
95 | }
96 |
97 | let id = shorthash(url);
98 | let cache = flatCache.load(`eleventy-plugin-img-${id}`, path.resolve(options.cacheDirectory));
99 | let cacheObj = cache.getKey(url);
100 |
101 | if(cacheObj && (options.duration === "*" || (Date.now() - cacheObj.cachedAt < getDurationMs(options.duration))) ) {
102 | resolve(cacheObj.value);
103 | } else {
104 | if(options.offlineMode) {
105 | resolve([]);
106 | return;
107 | }
108 |
109 | // TODO check to see if this is a local or remote url automatically
110 | let avatarCache = new AvatarLocalCache();
111 | let formats = getFormatsArray(options.formats);
112 | if(formats.length) {
113 | avatarCache.formats = formats;
114 | }
115 |
116 | let maxwidth = parseInt(options.maxwidth, 10);
117 | if(!isNaN(maxwidth)) {
118 | avatarCache.width = maxwidth;
119 | }
120 | if(!options.addWidthHeight) {
121 | avatarCache.skipMetadata = true;
122 | }
123 |
124 | // TODO make sure the imgDirectory exists.
125 | avatarCache.fetchUrl(url, path.join(options.imgDirectory, id)).catch(e => {
126 | console.log( `Image error: ${e}` );
127 | }).then(function(files) {
128 | if(files && files.length) {
129 | let paths = files.map(file => file.path);
130 | console.log( `Cached remote image from ${url} to:`, paths );
131 |
132 | // If we want this later, save it to a cache
133 | cache.setKey(url, {
134 | cachedAt: Date.now(),
135 | value: files
136 | });
137 | cache.save();
138 |
139 | resolve(files);
140 | } else {
141 | // Bad error code
142 | // TODO: option to resolve to stock image path?
143 | resolve([]);
144 | }
145 | });
146 | }
147 | }).then(function(files) {
148 | function getImgSrc(fileObj, options) {
149 | if(isFullUrl(fileObj.path)) {
150 | return fileObj.path;
151 | }
152 |
153 | return path.join( options.pathPrefix, options.imgSrcDirectory, `${fileObj.name}.${fileObj.extension}`);
154 | }
155 |
156 | let ret = [];
157 | if( files.length >= 2 ) {
158 |
159 | ret.push(``);
160 | ret.push(``);
161 | }
162 | if( files.length ) {
163 | let img = files[files.length - 1];
164 | let imgUrl = getImgSrc(img, options);
165 |
166 | if(options.addWidthHeight) {
167 | if(img.width && !props.width) {
168 | props.width = img.width;
169 | }
170 | if(img.height && !props.height) {
171 | props.height = img.height;
172 | }
173 | }
174 | let copiedProps = Object.assign({}, props);
175 | delete copiedProps.src;
176 |
177 | ret.push(`
`);
178 | }
179 | if( files.length >= 2 ) {
180 | ret.push("");
181 | }
182 |
183 | return ret.join("");
184 | }.bind(this));
185 | }
186 |
187 | module.exports = function(eleventyConfig) {
188 | eleventyConfig.addShortcode("img", imgShortcode);
189 | };
190 |
191 | module.exports.img = imgShortcode;
--------------------------------------------------------------------------------