├── .eslintrc.js ├── .github └── dependabot.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── assets └── images │ ├── diplomees2021.jpg │ ├── groupe-diplome-sejour-folle.jpg │ └── sejour-plein-air.jpg ├── index.js ├── package-lock.json ├── package.json ├── test-eleventy ├── default │ └── index.md └── relative-image │ ├── diplomees2021.jpg │ └── index.md ├── test.js └── utilities ├── errors.js ├── generate-attrs-object.js ├── lower-case-trim-object.js ├── remove-key-from.js └── warnings.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true 5 | }, 6 | extends: "eslint:recommended", 7 | parserOptions: { 8 | sourceType: "module", 9 | ecmaVersion: 2018 10 | }, 11 | rules: { 12 | indent: ["error", 2, { "SwitchCase": 1 }], 13 | "linebreak-style": ["error", "unix"], 14 | semi: ["error", "always"], 15 | "no-unused-vars": ["warn"], 16 | quotes: ["error", "double", { "avoidEscape": true }] 17 | } 18 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "npm" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | ignore: 10 | - dependency-name: "markdown-it" 11 | versioning-strategy: increase 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Mac 107 | .DS_Store 108 | 109 | # Eleventy (11ty) 110 | _site 111 | img -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at mathieu.huot@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 72 | version 2.1, available at 73 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 74 | 75 | For answers to common questions about this code of conduct, see the FAQ at 76 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 77 | [https://www.contributor-covenant.org/translations][translations]. 78 | 79 | [homepage]: https://www.contributor-covenant.org 80 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 81 | [Mozilla CoC]: https://github.com/mozilla/diversity 82 | [FAQ]: https://www.contributor-covenant.org/faq 83 | [translations]: https://www.contributor-covenant.org/translations 84 | 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # markdown-it-eleventy-img 2 | A [markdown-it](https://github.com/markdown-it/markdown-it) plugin that processes images through the [eleventy-img](https://github.com/11ty/eleventy-img) plugin. Can be used in any projects that use markdown-it. Fully compatible with the [Eleventy](https://www.11ty.dev/) static site generator. 3 | 4 | [![NPM version badge.](https://img.shields.io/npm/v/markdown-it-eleventy-img)](https://www.npmjs.com/package/markdown-it-eleventy-img) 5 | [![GitHub issues badge.](https://img.shields.io/github/issues/solution-loisir/markdown-it-eleventy-img)](https://github.com/solution-loisir/markdown-it-eleventy-img/issues) 6 | [![NPM license badge.](https://img.shields.io/npm/l/markdown-it-eleventy-img)](https://github.com/solution-loisir/markdown-it-eleventy-img/blob/master/LICENSE) 7 | 8 | ## Status 9 | 10 | This is pre-release software. There might still be API changes. I'm pretty much developing this plugin in the open and I'm learning buckets! 11 | 12 | **Use at your own risk**, but feel free to get in touch if you have questions, a comment or want to talk about your experience as a user. 13 | 14 | ## Requirements 15 | 16 | Same as [eleventy-img](https://github.com/11ty/eleventy-img). Was tested with markdown-it `v9.0.0` and Eleventy `v1.0.1`. 17 | 18 | ## Installation 19 | 20 | ``` 21 | npm install --save-dev markdown-it-eleventy-img 22 | ``` 23 | 24 | ## Usage 25 | 26 | This plugin is zero-config by default and can be use as a regular [markdown-it plugin](https://github.com/markdown-it/markdown-it#plugins-load). 27 | 28 | ```js 29 | const md = require('markdown-it')() 30 | .use(require("markdown-it-eleventy-img")); 31 | ``` 32 | With [Eleventy](https://www.11ty.dev/) (11ty), use the [config API](https://www.11ty.dev/docs/config/) `setLibrary` method. 33 | ```js 34 | // .eleventy.js (or your config file.) 35 | 36 | const markdownIt = require('markdown-it'); 37 | const markdownItEleventyImg = require("markdown-it-eleventy-img"); 38 | 39 | module.exports = function(config) { 40 | config.setLibrary('md', markdownIt ({ 41 | html: true, 42 | breaks: true, 43 | linkify: true 44 | }) 45 | .use(markdownItEleventyImg); 46 | } 47 | ``` 48 | Using markdown-it-eleventy-img without options will run eleventy-img with [default options](https://www.11ty.dev/docs/plugins/image/#usage) which are: 49 | 50 | * `widths: [null]` 51 | * `formats: ["webp", "jpeg"]` 52 | * `urlPath: "/img/"` 53 | * `outputDir: "./img/"` 54 | 55 | So using: 56 | 57 | ```md 58 | 59 | 60 | ![Image alt](./img/my-image.jpg "Title text!") 61 | ``` 62 | 63 | Will output: 64 | 65 | ```html 66 |

Image alt

67 | ``` 68 | By default, images are rendered using the `generateHTML` function from the eleventy-img plugin with the `whitespaceMode` "to strip the whitespace from the output of the `` element (a must-have for use in markdown files)" ([reference](https://www.11ty.dev/docs/plugins/image/#use-this-in-your-templates)). 69 | 70 | You can add an options object to override eleventy-img defaults, add global attributes or provide your own custom rendering. 71 | 72 | ## Using options 73 | 74 | The options object may contain these properties: 75 | * `imgOptions` (object). 76 | Overrides eleventy-img specific options. 77 | * `globalAttributes` (object). 78 | Adds attributes to the image output. 79 | * `renderImage` (function). 80 | Lets you render custom markup and do almost everything you like with your markdown images (see [Custom image rendering](#custom-image-rendering)). 81 | * `resolvePath` (function). 82 | Lets you decide how you want to resolve paths for images. By default, `markdown-it-eleventy-img` will resolve path against project root (see [Resolving path](#resolving-path)). 83 | 84 | Here's an exemple of using the options object: 85 | 86 | ```js 87 | .use(markdownItEleventyImg, { 88 | imgOptions: { 89 | widths: [800, 500, 300], 90 | urlPath: "/images/", 91 | outputDir: path.join("_site", "images"), 92 | formats: ["avif", "webp", "jpeg"] 93 | }, 94 | globalAttributes: { 95 | class: "markdown-image", 96 | decoding: "async", 97 | // If you use multiple widths, 98 | // don't forget to add a `sizes` attribute. 99 | sizes: "100vw" 100 | } 101 | }); 102 | ``` 103 | With these options, the image `![Image alt](./img/my-image.jpg "Title text!")`, will be rendered as: 104 | 105 | ```html 106 |

Image alt

107 | ``` 108 | The `alt`, the `src`, and the `title` attributes are taken from the markdown token: `![alt](./source.ext "Title")`. 109 | 110 | Setting `alt` or `src` properties in `globalAttributes` will have no effect on the markdown image output. These attributes have to be set on the markdown image token instead. 111 | 112 | A warning will be logged in the console if these properties are set in `globalAttributes`. 113 | 114 | The `title` property can be set in `globalAttributes`, but there's probably little insentive to do so. Attributes set on the token will always override global attributes. 115 | 116 | ## Custom image rendering 117 | 118 | You may use the `renderImage` method to customize the output or to add logic to your image rendering. This function is returned inside of the image renderer so any string returned by `renderImage` will render the markup for every image tokens. 119 | 120 | ```js 121 | renderImage(image, attributes) { 122 | const [ src, attrs ] = attributes; 123 | const alt = attrs.alt; 124 | 125 | return `${alt}`; 126 | } 127 | ``` 128 | 129 | `renderImage` takes two parameters and each one are tuples. 130 | 131 | ```js 132 | renderImage(image, attributes) { 133 | const [ Image, options ] = image; 134 | const [ src, attrs ] = attributes; 135 | 136 | // ... 137 | } 138 | ``` 139 | We separate the `src` form the rest of the attributes to facilitate the use of the `Image` class. If you need to, it is simple to reunite. 140 | 141 | ```js 142 | const [ src, attrs ] = attributes; 143 | const allAttributes = { ...attrs, src } 144 | ``` 145 | 146 | Here's an exemple of adding a `
` parent and an optional `
`. 147 | 148 | ```js 149 | .use(markdownItEleventyImg, { 150 | imgOptions: { 151 | widths: [600, 300], 152 | urlPath: "/images/", 153 | outputDir: "./_site/images/", 154 | formats: ["avif", "webp", "jpeg"] 155 | }, 156 | globalAttributes: { 157 | class: "markdown-image", 158 | decoding: "async", 159 | sizes: "100vw" 160 | }, 161 | renderImage(image, attributes) { 162 | const [ Image, options ] = image; 163 | const [ src, attrs ] = attributes; 164 | 165 | Image(src, options); 166 | 167 | const metadata = Image.statsSync(src, options); 168 | const imageMarkup = Image.generateHTML(metadata, attrs, { 169 | whitespaceMode: "inline" 170 | }); 171 | 172 | return `
${imageMarkup}${attrs.title ? `
${attrs.title}
` : ""}
`; 173 | } 174 | }); 175 | ``` 176 | > Note that you have to use eleventy-img [synchronous API](https://www.11ty.dev/docs/plugins/image/#synchronous-shortcode) inside `renderImage`. Unfortunately, markdown-it plugins doesn't support async code (see [Limitations](#limitations)). It's good to know that even in the sync API, the images are generated asychronously. Got to 😍 11ty! 177 | 178 | ## Resolving path 179 | By default, `markdown-it-eleventy-img` will resolve relative paths against current working directory (project root). Use `resolvePath` to configure how paths are resolved. Returned value: the source path. 180 | 181 | The `resolvePath` method takes two arguments: 182 | 1. The image source from the token. 183 | 2. The `env` parameter from the Markdown-it renderer. 184 | 185 | Here's an example of resolving path relative to the markdown file in Eleventy: 186 | ```js 187 | config.setLibrary("md", md.use(markdownItEleventyImg, { 188 | resolvePath: (filepath, env) => path.join(path.dirname(env.page.inputPath), filepath) 189 | })); 190 | ``` 191 | Since Eleventy is [passing its supplied data to `env`](https://github.com/11ty/eleventy/issues/1510#issuecomment-1046128400), this is possible. It may or may not be possible in other context. It was only tested with Eleventy. 192 | 193 | ## Use with markdown-it-attrs 194 | 195 | Starting with `v0.3.0`, markdown-it-eleventy-img is fully compatible with [markdown-it-attrs](https://www.npmjs.com/package/markdown-it-attrs). Setting attributes with markdown-it-attrs will be passed to the image output. Same attributes will be overridden. The attribute set on the token will prevail over `globalAttributes`. 196 | 197 | ## Limitations 198 | 199 | markdown-it-eleventy-img is currently **not supported with remote sources**. Starting with `v0.7.0`, the default behavior will be to revert to native markdown-it renderer whenever remote images are encountered (local sources will still be rendered through eleventy-img). 200 | 201 | To process remote images, use code where the eleventy-img [async API](https://www.11ty.dev/docs/plugins/image/#asynchronous-shortcode) is allowed. For a more technical expaination see [issue#2](https://github.com/solution-loisir/markdown-it-eleventy-img/issues/2). 202 | 203 | ## Alignements 204 | 205 | markdown-it-eleventy-img tries to follow as much as possible the [markdown philosophy](https://daringfireball.net/projects/markdown/syntax#philosophy). The idea is to keep simplicity while generating responsive image format in markdown. With markdown-it-eleventy-img, you author images in markdown the same way as usual. 206 | 207 | Also, the aim is to stay coherent with [eleventy-img](https://www.11ty.dev/docs/plugins/image/) defaults while allowing users to use the full feature set. So if your familiar with the eleventy-img plugin, there should be no surprises. 208 | 209 | markdown-it-eleventy-img is not a replacement for an Eleventy [shortcode](https://www.11ty.dev/docs/shortcodes/). Shortcodes will give more power and control. On the other hand, markdown will trade control for easiness of reading and writing. 210 | 211 | [Admittedly, it’s fairly difficult to devise a “natural” syntax for placing images into a plain text document format.](https://daringfireball.net/projects/markdown/syntax#img) That beiing said, it's probably nicer to use markdown image syntax to include an image in a blog post than to use a shortcode. So markdown-it-eleventy-img is mainly an ergonomic solution for using eleventy-img plugin with the markdown image synthax. 212 | 213 | ## Motivation 214 | 215 | I've seen it asked [here](https://github.com/AleksandrHovhannisyan/aleksandrhovhannisyan.com/issues/118#issuecomment-1190703611) and [there](https://github.com/11ty/eleventy-img/issues/90): can I use the eleventy-img plugin with markdown token? I thought it was a good challenge and I ended-up publishing my work. -------------------------------------------------------------------------------- /assets/images/diplomees2021.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solution-loisir/markdown-it-eleventy-img/26b4a4ef7efdb028ef5acfa197d493bd1d91cf67/assets/images/diplomees2021.jpg -------------------------------------------------------------------------------- /assets/images/groupe-diplome-sejour-folle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solution-loisir/markdown-it-eleventy-img/26b4a4ef7efdb028ef5acfa197d493bd1d91cf67/assets/images/groupe-diplome-sejour-folle.jpg -------------------------------------------------------------------------------- /assets/images/sejour-plein-air.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solution-loisir/markdown-it-eleventy-img/26b4a4ef7efdb028ef5acfa197d493bd1d91cf67/assets/images/sejour-plein-air.jpg -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Image = require("@11ty/eleventy-img"); 2 | const logWarningFor = require("./utilities/warnings"); 3 | const { remove } = require("./utilities/remove-key-from"); 4 | const generateAttrsObject = require("./utilities/generate-attrs-object"); 5 | const { typeObjectError, typeFunctionError } = require("./utilities/errors"); 6 | const { propertiesFrom } = require("./utilities/lower-case-trim-object"); 7 | 8 | /** 9 | * 10 | * @param {MarkdownIt} md The markdown-it object 11 | * @param {*} imgOptions Overrides eleventy-img specific options. 12 | * @param {*} globalAttributes Adds attributes to the image output. 13 | * @param {Function} renderImage Lets you render custom markup and do almost everything you like with your markdown images. 14 | * @param {Function} resolvePath Function that will be used to resolve paths for images in markdown. Receives image path string and env as parameters. Default resolves to CWD. 15 | */ 16 | 17 | module.exports = function markdownItEleventyImg(md, { 18 | imgOptions = {}, 19 | globalAttributes = {}, 20 | renderImage, 21 | resolvePath 22 | } = {}) { 23 | 24 | typeObjectError(imgOptions, "imgOptions"); 25 | typeObjectError(globalAttributes, "globalAttributes"); 26 | typeFunctionError(renderImage, "renderImage"); 27 | typeFunctionError(resolvePath, "resolvePath"); 28 | 29 | const normalizedGlobalAttributes = propertiesFrom(globalAttributes).lowerCased().trimmed().object(); 30 | 31 | logWarningFor(normalizedGlobalAttributes); 32 | 33 | md.renderer.rules.image = (tokens, index, rendererOptions, env, renderer) => { 34 | 35 | const token = tokens[index]; 36 | 37 | // Passing remote sources through Markdown-it default renderer. 38 | if(Image.Util.isRemoteUrl(token.attrGet("src"))) { 39 | token.attrs[token.attrIndex("alt")][1] = token.content; 40 | return renderer.renderToken(tokens, index, rendererOptions); 41 | } 42 | 43 | const normalizedTokenAttributes = generateAttrsObject(token).addContentTo("alt").attrs; 44 | 45 | const src = (resolvePath) ? resolvePath(normalizedTokenAttributes.src, env) : normalizedTokenAttributes.src; 46 | 47 | const normalizedTokenAttributesWithoutSrc = remove("src").from(normalizedTokenAttributes); 48 | 49 | const imageAttributes = { ...normalizedGlobalAttributes, ...normalizedTokenAttributesWithoutSrc }; 50 | 51 | if(renderImage) { 52 | const image = [ Image, imgOptions ]; 53 | const attributes = [ src, imageAttributes ]; 54 | return renderImage(image, attributes); 55 | } 56 | 57 | Image(src, imgOptions); 58 | 59 | const metadata = Image.statsSync(src, imgOptions); 60 | const imageMarkup = Image.generateHTML(metadata, imageAttributes, { 61 | whitespaceMode: "inline" 62 | }); 63 | 64 | return imageMarkup; 65 | }; 66 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdown-it-eleventy-img", 3 | "description": "A markdown-it plugin that processes images through the eleventy-img plugin. Can be used in any projects that use markdown-it. Fully compatible with the Eleventy static site generator.", 4 | "version": "0.10.2", 5 | "main": "index.js", 6 | "publishConfig": { 7 | "access": "public" 8 | }, 9 | "scripts": { 10 | "test": "eslint index.js test.js utilities/*.js && ava", 11 | "check-updates": "npm outdated" 12 | }, 13 | "engines": { 14 | "node": ">=18" 15 | }, 16 | "devDependencies": { 17 | "@11ty/eleventy": "^2.0.1", 18 | "ava": "^6.1.2", 19 | "eslint": "^8.57.0", 20 | "markdown-it": "^9.0.0", 21 | "markdown-it-attrs": "^4.1.6", 22 | "markdown-it-implicit-figures": "^0.12.0" 23 | }, 24 | "dependencies": { 25 | "@11ty/eleventy-img": "^4.0.2", 26 | "sharp": "^0.33.2" 27 | }, 28 | "peerDependencies": { 29 | "markdown-it": ">= 9.0.0" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/solution-loisir/markdown-it-eleventy-img.git" 34 | }, 35 | "keywords": [ 36 | "markdown", 37 | "markdown-it", 38 | "markdown-it-plugin", 39 | "eleventy", 40 | "11ty", 41 | "eleventy-utility", 42 | "eleventy-plugin", 43 | "eleventy-img" 44 | ], 45 | "author": { 46 | "name": "Mathieu Huot", 47 | "email": "mathieu.huot@gmail.com" 48 | }, 49 | "license": "Apache-2.0", 50 | "bugs": { 51 | "url": "https://github.com/solution-loisir/markdown-it-eleventy-img/issues" 52 | }, 53 | "homepage": "https://github.com/solution-loisir/markdown-it-eleventy-img#readme" 54 | } 55 | -------------------------------------------------------------------------------- /test-eleventy/default/index.md: -------------------------------------------------------------------------------- 1 | ![Alt diplomees2021](./assets/images/diplomees2021.jpg "Title diplomees2021") 2 | 3 | -------------------------------------------------------------------------------- /test-eleventy/relative-image/diplomees2021.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solution-loisir/markdown-it-eleventy-img/26b4a4ef7efdb028ef5acfa197d493bd1d91cf67/test-eleventy/relative-image/diplomees2021.jpg -------------------------------------------------------------------------------- /test-eleventy/relative-image/index.md: -------------------------------------------------------------------------------- 1 | ![Alt diplomees2021](./diplomees2021.jpg "Title diplomees2021") 2 | 3 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const md = require("markdown-it")(); 3 | const markdownItAttrs = require("markdown-it-attrs"); 4 | const implicitFigures = require("markdown-it-implicit-figures"); 5 | const markdownItEleventyImg = require("./"); 6 | const logWarningFor = require("./utilities/warnings"); 7 | const generateAttrsObject = require("./utilities/generate-attrs-object"); 8 | const { remove } = require("./utilities/remove-key-from"); 9 | const { propertiesFrom } = require("./utilities/lower-case-trim-object"); 10 | const fs = require("fs"); 11 | const path = require("path"); 12 | const Eleventy = require("@11ty/eleventy"); 13 | const eleventyInput = "test-eleventy/default"; 14 | const eleventyInputRelative = "test-eleventy/relative-image"; 15 | const eleventyOutput = "_site"; 16 | const imageDiplomees2021 = '![Alt diplomees2021](./assets/images/diplomees2021.jpg "Title diplomees2021")'; 17 | const imagemarkdownItAttrs = '![Alt diplomees2021](./assets/images/diplomees2021.jpg "Title diplomees2021"){loading=lazy}'; 18 | const imgWithoutTitle = "![Alt diplomees2021](./assets/images/diplomees2021.jpg)"; 19 | const imgWithEmptyTitle = '![Alt diplomees2021](./assets/images/diplomees2021.jpg "")'; 20 | const remoteSrc = "https://apod.nasa.gov/apod/image/2208/StargateMilkyWay_Oudoux_1800.jpg"; 21 | const remoteSrc_1 = "https://www.nasa.gov/sites/default/files/thumbnails/image/web_first_images_release.png"; 22 | const remoteSrc_2 = "https://www.nasa.gov/sites/default/files/thumbnails/image/main_image_deep_field_smacs0723-5mb.jpg"; 23 | const remoteImage = `![](${remoteSrc})`; 24 | const remoteImageAlt = `![My cool space pic](${remoteSrc})`; 25 | const remoteImageAltAndTitle = `![My cool space pic](${remoteSrc} "Remote title")`; 26 | const multipleRemoteImages = `![First alt](${remoteSrc} "First title")\n![Second alt](${remoteSrc_1} "Second title")\n![Third alt](${remoteSrc_2} "Third title")`; 27 | const multipleLocalImages = `${imageDiplomees2021}\n![Alt sejour](./assets/images/sejour-plein-air.jpg "Title sejour")`; 28 | const markdownItAttrsWidthAndHeight = "![](./assets/images/diplomees2021.jpg){width=200 height=100}"; 29 | 30 | test("Not passing global width and height to local images", t => { 31 | const result = md.use(markdownItEleventyImg, { 32 | imgOptions: { 33 | dryRun: true 34 | }, 35 | globalAttributes: { 36 | width: 1800, 37 | height: 900 38 | } 39 | }).render(multipleLocalImages); 40 | 41 | t.is(result, '

Alt diplomees2021\n' + 42 | 'Alt sejour

\n'); 43 | }); 44 | 45 | test("Global width and height are present in the attributes object", t => { 46 | md.use(markdownItEleventyImg, { 47 | imgOptions: { 48 | dryRun: true 49 | }, 50 | globalAttributes: { 51 | width: 1800, 52 | height: 900 53 | }, 54 | renderImage(image, attributes) { 55 | const [src, attrs] = attributes; 56 | t.is(attrs.width, 1800); 57 | t.is(attrs.height, 900); 58 | } 59 | }).render(multipleLocalImages); 60 | }); 61 | 62 | test("Empty string title is undefined", t => { 63 | md.use(markdownItEleventyImg, { 64 | imgOptions: { 65 | dryRun: true 66 | }, 67 | renderImage(image, attributes) { 68 | const [src, attrs] = attributes; 69 | t.is(attrs.title, undefined); 70 | } 71 | }).render(imgWithEmptyTitle); 72 | }); 73 | 74 | test("Image without title (title is undefined)", t => { 75 | md.use(markdownItEleventyImg, { 76 | imgOptions: { 77 | dryRun: true 78 | }, 79 | renderImage(image, attributes) { 80 | const [src, attrs] = attributes; 81 | t.is(attrs.title, undefined); 82 | } 83 | }).render(imgWithoutTitle); 84 | }); 85 | 86 | test.serial("Title in globalAttributes is overriden by token title", t => { 87 | const result = md.use(markdownItEleventyImg, { 88 | imgOptions: { 89 | dryRun: true 90 | }, 91 | globalAttributes: { 92 | " Title": "Global title" 93 | } 94 | }).render(imageDiplomees2021); 95 | 96 | t.is(result, '

Alt diplomees2021

\n'); 97 | }); 98 | 99 | test.serial("Title in globalAttributes is passed on token with empty title", t => { 100 | const result = md.use(markdownItEleventyImg, { 101 | imgOptions: { 102 | dryRun: true 103 | }, 104 | globalAttributes: { 105 | "TITLE": "" 106 | } 107 | }).render(imgWithEmptyTitle); 108 | 109 | t.is(result, '

Alt diplomees2021

\n'); 110 | }); 111 | 112 | test.serial("Title in globalAttributes is applied when no token title", t => { 113 | const result = md.use(markdownItEleventyImg, { 114 | imgOptions: { 115 | dryRun: true 116 | }, 117 | globalAttributes: { 118 | title: "Global title" 119 | } 120 | }).render(imgWithoutTitle); 121 | 122 | t.is(result, '

Alt diplomees2021

\n'); 123 | }); 124 | 125 | test("Log warning for alt", t => { 126 | const globalAttributes = { 127 | alt: "" 128 | }; 129 | 130 | t.is(logWarningFor(globalAttributes), "Markdown-it-eleventy-img WARNING: Setting `alt` in `globalAttributes` will have no effect on the markdown image output. The `alt` attribute has to be set on the markdown image token."); 131 | }); 132 | 133 | test("Log warning for src", t => { 134 | const globalAttributes = { 135 | src: "" 136 | }; 137 | 138 | t.is(logWarningFor(globalAttributes), "Markdown-it-eleventy-img WARNING: Setting `src` in `globalAttributes` will have no effect on the markdown image output. The `src` attribute has to be set on the markdown image token."); 139 | }); 140 | 141 | test("typeObjectError for imgOptions (string)", t => { 142 | t.throws(() => { 143 | md.use(markdownItEleventyImg, { 144 | imgOptions: "" 145 | }); 146 | }, {message: "Markdown-it-eleventy-img: `imgOptions` needs to be an `object`."}); 147 | }); 148 | 149 | test("typeObjectError for imgOptions (number)", t => { 150 | t.throws(() => { 151 | md.use(markdownItEleventyImg, { 152 | imgOptions: 1 153 | }); 154 | }, {message: "Markdown-it-eleventy-img: `imgOptions` needs to be an `object`."}); 155 | }); 156 | 157 | test("typeObjectError for imgOptions (array)", t => { 158 | t.throws(() => { 159 | md.use(markdownItEleventyImg, { 160 | imgOptions: [] 161 | }); 162 | }, {message: "Markdown-it-eleventy-img: `imgOptions` needs to be an `object`."}); 163 | }); 164 | 165 | test("typeObjectError for imgOptions (null)", t => { 166 | t.throws(() => { 167 | md.use(markdownItEleventyImg, { 168 | imgOptions: null 169 | }); 170 | }, {message: "Markdown-it-eleventy-img: `imgOptions` needs to be an `object`."}); 171 | }); 172 | 173 | test("typeObjectError for imgOptions (undefined not throwing)", t => { 174 | t.notThrows(() => { 175 | md.use(markdownItEleventyImg, { 176 | imgOptions: undefined 177 | }); 178 | }); 179 | }); 180 | 181 | test("typeFunctionError for `resolvePath` (string throwing)", t => { 182 | t.throws(() => { 183 | md.use(markdownItEleventyImg, { 184 | resolvePath: "string" 185 | }); 186 | }); 187 | }); 188 | 189 | test("typeFunctionError for `renderImage` (string throwing)", t => { 190 | t.throws(() => { 191 | md.use(markdownItEleventyImg, { 192 | renderImage: "string" 193 | }); 194 | }); 195 | }); 196 | 197 | test("generate-attrs-object", t => { 198 | const tokens = md.parseInline(imageDiplomees2021); 199 | const token = tokens[0].children[0]; 200 | 201 | const tokenAttributes = generateAttrsObject(token).attrs; 202 | 203 | t.deepEqual(tokenAttributes, { 204 | src: "./assets/images/diplomees2021.jpg", 205 | alt: "", 206 | title: "Title diplomees2021" 207 | }); 208 | 209 | const tokenAttributesWithAlt = generateAttrsObject(token).addContentTo("alt").attrs; 210 | 211 | t.deepEqual(tokenAttributesWithAlt, { 212 | src: "./assets/images/diplomees2021.jpg", 213 | alt: "Alt diplomees2021", 214 | title: "Title diplomees2021" 215 | }); 216 | }); 217 | 218 | test("generate-attrs-object (trimmed and lower case result)", t => { 219 | const tokenArray = { attrs: [[" SRC", "my/src/"], ["Alt ", "This alt test"]] }; 220 | 221 | const output = generateAttrsObject(tokenArray).attrs; 222 | 223 | t.deepEqual(output, { 224 | "src": "my/src/", 225 | "alt": "This alt test" 226 | }); 227 | }); 228 | 229 | test("remove-key-from", t => { 230 | const tokenAttributes = { 231 | src: "./assets/images/diplomees2021.jpg", 232 | alt: "Alt diplomees2021", 233 | title: "Title diplomees2021" 234 | }; 235 | 236 | const tokenAttributesWithoutSrc = remove("src").from(tokenAttributes); 237 | 238 | t.deepEqual(tokenAttributesWithoutSrc, { 239 | alt: "Alt diplomees2021", 240 | title: "Title diplomees2021" 241 | }); 242 | }); 243 | 244 | test("propertiesFrom", t => { 245 | const testObject = { 246 | " SRC ": "my/source/img.jpg", 247 | " Title ": "My title" 248 | }; 249 | 250 | const result = propertiesFrom(testObject).lowerCased().trimmed().object(); 251 | 252 | t.deepEqual(result, { 253 | src: "my/source/img.jpg", 254 | title: "My title" 255 | }); 256 | }); 257 | 258 | test("markdown-it-attrs overrides globalAttributes (typed string)", t => { 259 | md.use(markdownItAttrs) 260 | .use(markdownItEleventyImg, { 261 | imgOptions: { 262 | dryRun: true 263 | }, 264 | globalAttributes: { 265 | width: 1800, 266 | height: 900 267 | }, 268 | renderImage(image, attributes) { 269 | const [src, attrs] = attributes; 270 | t.true(typeof attrs.width === "string"); 271 | t.is(attrs.width, "200"); 272 | t.true(typeof attrs.height === "string"); 273 | t.is(attrs.height, "100"); 274 | } 275 | }).render(markdownItAttrsWidthAndHeight); 276 | }); 277 | 278 | test("Width and height attributes are not passed to local image output (from token or from config)", t => { 279 | const result = md 280 | .use(markdownItAttrs) 281 | .use(markdownItEleventyImg, { 282 | imgOptions: { 283 | dryRun: true 284 | }, 285 | globalAttributes: { 286 | width: 1800, 287 | height: 900 288 | } 289 | }).render(markdownItAttrsWidthAndHeight); 290 | 291 | t.is(result, '

\n'); 292 | }); 293 | 294 | test("markdown-it-attrs pass down attributes", t => { 295 | const result = md 296 | .use(markdownItEleventyImg) 297 | .use(markdownItAttrs) 298 | .render(imagemarkdownItAttrs); 299 | 300 | t.is(result, '

Alt diplomees2021

\n'); 301 | }); 302 | 303 | test("markdown-it-attrs pass overrides attributes", t => { 304 | const result = md 305 | .use(markdownItEleventyImg, { 306 | globalAttributes: { 307 | loading: "eager" 308 | } 309 | }) 310 | .use(markdownItAttrs) 311 | .render(imagemarkdownItAttrs); 312 | 313 | t.is(result, '

Alt diplomees2021

\n'); 314 | }); 315 | 316 | test.serial("markdownItEleventyImg with markdown-it (default no-config)", t => { 317 | const result = md.use(markdownItEleventyImg).render(imageDiplomees2021); 318 | 319 | t.is(result, '

Alt diplomees2021

\n'); 320 | }); 321 | 322 | test.serial("markdownItEleventyImg with markdown-it with imgOptions and globalAttributes (dryrun)", t => { 323 | const result = md.use(markdownItEleventyImg, { 324 | imgOptions: { 325 | widths: [800, 500, 300], 326 | urlPath: "/images/", 327 | outputDir: path.join("_site", "images"), 328 | formats: ["avif", "webp", "jpeg"], 329 | dryRun: true 330 | }, 331 | globalAttributes: { 332 | class: "markdown-image", 333 | decoding: "async", 334 | sizes: "100vw" 335 | } 336 | }).render(imageDiplomees2021); 337 | 338 | t.is(result, '

Alt diplomees2021

\n'); 339 | }); 340 | 341 | test.serial("markdownItEleventyImg with markdown-it with renderImage (dryrun)", t => { 342 | const result = md.use(markdownItEleventyImg, { 343 | imgOptions: { 344 | dryRun: true 345 | }, 346 | renderImage(image, attributes) { 347 | const [ Image, options ] = image; 348 | const [ src, attrs ] = attributes; 349 | 350 | t.true(typeof Image === "function"); 351 | t.true(typeof options === "object" && options.dryRun); 352 | t.true(typeof src === "string"); 353 | t.true(typeof attrs === "object" && !attrs.src); 354 | 355 | Image(src, options); 356 | 357 | const metadata = Image.statsSync(src, options); 358 | 359 | t.true(typeof metadata === "object"); 360 | 361 | const imageMarkup = Image.generateHTML(metadata, attrs, { 362 | whitespaceMode: "inline" 363 | }); 364 | 365 | return `
${imageMarkup}${attrs.title ? `
${attrs.title}
` : ""}
`; 366 | } 367 | }).render(imageDiplomees2021); 368 | 369 | t.is(result, '

Alt diplomees2021
Title diplomees2021

\n'); 370 | }); 371 | 372 | test.serial("markdownItEleventyImg with Eleventy (default no-config)", async t => { 373 | let elev = new Eleventy(eleventyInput, eleventyOutput, { 374 | config(config) { 375 | config.setLibrary("md", md.use(markdownItEleventyImg)); 376 | } 377 | }); 378 | let json = await elev.toJSON(); 379 | 380 | t.is(json[0].content, '

Alt diplomees2021

\n'); 381 | }); 382 | 383 | test.serial("markdownItEleventyImg with Eleventy using custom image path resolution for relative images.", async t => { 384 | let elev = new Eleventy(eleventyInputRelative, eleventyOutput, { 385 | config(config) { 386 | config.setLibrary("md", md.use(markdownItEleventyImg, { 387 | resolvePath: (filepath, env) => path.join(path.dirname(env.page.inputPath), filepath) 388 | })); 389 | } 390 | }); 391 | let json = await elev.toJSON(); 392 | 393 | t.is(json[0].content, '

Alt diplomees2021

\n'); 394 | }); 395 | 396 | test.serial("markdownItEleventyImg with Eleventy using using `resolvePath` with remote images.", async t => { 397 | const result = md.use(markdownItEleventyImg, { 398 | resolvePath: (filepath, env) => path.join(path.dirname(env.page.inputPath), filepath) 399 | }).render(remoteImage); 400 | 401 | t.is(result, '

\n'); 402 | }); 403 | 404 | test.serial("markdownItEleventyImg with Eleventy with imgOptions and globalAttributes (dryrun)", async t => { 405 | let elev = new Eleventy(eleventyInput, eleventyOutput, { 406 | config(config) { 407 | config.setLibrary("md", md.use(markdownItEleventyImg, { 408 | imgOptions: { 409 | widths: [800, 500, 300], 410 | urlPath: "/images/", 411 | outputDir: path.join("_site", "images"), 412 | formats: ["avif", "webp", "jpeg"], 413 | dryRun: true 414 | }, 415 | globalAttributes: { 416 | class: "markdown-image", 417 | decoding: "async", 418 | sizes: "100vw" 419 | } 420 | })); 421 | } 422 | }); 423 | let json = await elev.toJSON(); 424 | 425 | t.is(json[0].content, '

Alt diplomees2021

\n'); 426 | }); 427 | 428 | test.serial("markdownItEleventyImg with Eleventy with renderImage (dryrun)", async t => { 429 | let elev = new Eleventy(eleventyInput, eleventyOutput, { 430 | config(config) { 431 | config.setLibrary("md", md.use(markdownItEleventyImg, { 432 | imgOptions: { 433 | dryRun: true 434 | }, 435 | renderImage(image, attributes) { 436 | const [ Image, options ] = image; 437 | const [ src, attrs ] = attributes; 438 | 439 | t.true(typeof Image === "function"); 440 | t.true(typeof options === "object" && options.dryRun); 441 | t.true(typeof src === "string"); 442 | t.true(typeof attrs === "object" && !attrs.src); 443 | 444 | Image(src, options); 445 | 446 | const metadata = Image.statsSync(src, options); 447 | 448 | t.true(typeof metadata === "object"); 449 | 450 | const imageMarkup = Image.generateHTML(metadata, attrs, { 451 | whitespaceMode: "inline" 452 | }); 453 | 454 | return `
${imageMarkup}${attrs.title ? `
${attrs.title}
` : ""}
`; 455 | } 456 | })); 457 | } 458 | }); 459 | let json = await elev.toJSON(); 460 | 461 | t.is(json[0].content, '

Alt diplomees2021
Title diplomees2021

\n'); 462 | }); 463 | 464 | test("Remote images not throwing error", t => { 465 | t.notThrows(() => { 466 | md.use(markdownItEleventyImg, { 467 | imgOptions: { 468 | dryRun: true 469 | } 470 | }).render(remoteImage); 471 | }); 472 | }); 473 | 474 | test.serial("Remote images falls back to default markdown-it renderer (no alt)", t => { 475 | const result = md 476 | .use(markdownItEleventyImg, { 477 | imgOptions: { 478 | dryRun: true 479 | } 480 | }).render(remoteImage); 481 | 482 | t.is(result, '

\n'); 483 | }); 484 | 485 | test.serial("Remote images properly pass through alt tags into customised markdown-it-renderer", t => { 486 | const result = md 487 | .use(markdownItEleventyImg, { 488 | imgOptions: { 489 | dryRun: true 490 | } 491 | }).render(remoteImageAlt); 492 | 493 | t.is(result, '

My cool space pic

\n'); 494 | }); 495 | 496 | test.serial("Remote images properly pass through alt and title", t => { 497 | const result = md 498 | .use(markdownItEleventyImg, { 499 | imgOptions: { 500 | dryRun: true 501 | } 502 | }).render(remoteImageAltAndTitle); 503 | 504 | t.is(result, '

My cool space pic

\n'); 505 | }); 506 | 507 | test.serial("Remote images properly pass through alt and title on multiple images", t => { 508 | const result = md 509 | .use(markdownItEleventyImg, { 510 | imgOptions: { 511 | dryRun: true 512 | } 513 | }).render(multipleRemoteImages); 514 | 515 | t.is(result, '

First alt\n' + 516 | 'Second alt\n' + 517 | 'Third alt

\n'); 518 | }); 519 | 520 | test.serial("markdown-it-implicit-figures with options (dryrun)", t => { 521 | const result = md 522 | .use(implicitFigures, { 523 | dataType: true, 524 | figcaption: true, 525 | tabindex: true, 526 | link: true 527 | }) 528 | .use(markdownItEleventyImg, { 529 | imgOptions: { 530 | dryRun: true 531 | } 532 | }).render(imageDiplomees2021); 533 | 534 | t.is(result, '
Alt diplomees2021
Alt diplomees2021
\n'); 535 | }); 536 | 537 | test.after("cleanup", () => { 538 | const imgDir = "img"; 539 | 540 | if(fs.existsSync(imgDir)) { 541 | fs.rmSync(imgDir, { recursive: true }); 542 | } 543 | }); -------------------------------------------------------------------------------- /utilities/errors.js: -------------------------------------------------------------------------------- 1 | const typeObjectError = (parameterValue, parameterName = "") => { 2 | if ( 3 | typeof parameterValue !== "object" || 4 | Array.isArray(parameterValue) || 5 | parameterValue === null 6 | ) throw new Error(`Markdown-it-eleventy-img: \`${parameterName}\` needs to be an \`object\`.`); 7 | }; 8 | 9 | const typeFunctionError = (parameterValue, parameterName = "") => { 10 | if( 11 | typeof parameterValue !== "function" && 12 | parameterValue !== undefined 13 | ) throw new Error(`Markdown-it-eleventy-img: \`${parameterName}\` needs to be a \`function\`.`); 14 | }; 15 | 16 | module.exports = { 17 | typeObjectError, 18 | typeFunctionError 19 | }; -------------------------------------------------------------------------------- /utilities/generate-attrs-object.js: -------------------------------------------------------------------------------- 1 | module.exports = token => { 2 | const tokenAttributes = token.attrs.reduce((acc, current) => { 3 | const trimmedLowerCasedKey = current[0].toLowerCase().trim(); 4 | acc[trimmedLowerCasedKey] = current[1]; 5 | return acc; 6 | }, {}); 7 | 8 | return { 9 | attrs: tokenAttributes, 10 | addContentTo(key = "") { 11 | this.attrs[key] = token.content; 12 | return this; 13 | } 14 | }; 15 | }; -------------------------------------------------------------------------------- /utilities/lower-case-trim-object.js: -------------------------------------------------------------------------------- 1 | const propertiesFrom = sourceObject => { 2 | const _sourceKeys = Object.keys(sourceObject); 3 | let _keys = Object.keys(sourceObject); 4 | 5 | return { 6 | lowerCased() { 7 | _keys = _keys.map(key => key.toLowerCase()); 8 | return this; 9 | }, 10 | trimmed() { 11 | _keys = _keys.map(key => key.trim()); 12 | return this; 13 | }, 14 | object() { 15 | return _keys.reduce((newObject, key, index) => { 16 | newObject[key] = sourceObject[_sourceKeys[index]]; 17 | return newObject; 18 | }, {}); 19 | } 20 | }; 21 | }; 22 | 23 | module.exports = { 24 | propertiesFrom 25 | }; -------------------------------------------------------------------------------- /utilities/remove-key-from.js: -------------------------------------------------------------------------------- 1 | const remove = (excludeValue = "") => ({ 2 | from(source = {}) { 3 | if(!source[excludeValue]) return source; 4 | 5 | return Object.keys(source).filter(key => key !== excludeValue).reduce((acc, current) => { 6 | acc[current] = source[current]; 7 | return acc; 8 | }, {}); 9 | } 10 | }); 11 | 12 | module.exports = { 13 | remove 14 | }; -------------------------------------------------------------------------------- /utilities/warnings.js: -------------------------------------------------------------------------------- 1 | module.exports = target => { 2 | let warning = ""; 3 | 4 | Object.keys(target).forEach(property => { 5 | switch(property) { 6 | case "alt": 7 | case "src": 8 | warning = `Markdown-it-eleventy-img WARNING: Setting \`${property}\` in \`globalAttributes\` will have no effect on the markdown image output. The \`${property}\` attribute has to be set on the markdown image token.`; 9 | console.warn(warning); 10 | break; 11 | } 12 | }); 13 | 14 | return warning; 15 | }; --------------------------------------------------------------------------------