├── .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; --------------------------------------------------------------------------------