├── .gitignore ├── LICENSE ├── README.md ├── gatsby-node.js ├── package-lock.json ├── package.json ├── src ├── index.ts ├── on-create-node.ts └── utils.ts ├── tsconfig.json └── types.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variable files 55 | .env* 56 | .vscode 57 | 58 | # gatsby files 59 | .cache/ 60 | public 61 | 62 | # Mac files 63 | .DS_Store 64 | 65 | # Build directory 66 | dist/ 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The BSD Zero Clause License (0BSD) 2 | 3 | Copyright (c) 2020 Gatsby Inc. 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 10 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 12 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 13 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 14 | PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🛠 Hello, I need a maintainer 👩‍💻 🧑‍💻 🛠 2 | 3 | As you can probably see, I have not maintained, or used this package (or Gatsby for that matter) in a number of years. Frankly, I'm surprised this package still needs to exist. I have no plans to continue working on this so if you would like to take over this repo please let me know. 4 | 5 | # gatsby-remark-relative-images 6 | 7 | Convert image src(s) in markdown/html/frontmatter to be relative to their node's parent directory. This will help [gatsby-remark-images](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-remark-images) match images outside the node folder. This was built for use with NetlifyCMS and should be considered a temporary solution until relative paths are supported. If it works for other use cases then great! 8 | 9 | ### Features 10 | 11 | - [x] Converts markdown/mdx images 12 | - [x] Converts `src` in markdown/mdx html `` tags 13 | - [x] Converts frontmatter fields, supports nested fields 14 | - [x] Suports Unicode characters 15 | - [x] Frontmatter field filters (include/exclude) 16 | 17 | ## v2 Breaking Changes: 18 | 19 | The `fmImagesToRelative()` function has been removed, it is no longer needed. 20 | 21 | NOTE: v2 greatly simplifies things and seems to work well for my use-case (NetlifyCMS), if you were previously using this plugin for something else that no longer works with v2, please open an issue and let me know and I will try to accomodate your use-case. Thanks. 22 | 23 | ## Install 24 | 25 | ```bash 26 | # Install v2 (Recommended) 27 | yarn add gatsby-remark-relative-images 28 | 29 | # Install v1 (TS refactor, but quickly found more things to simplfy, skip) 30 | npm i gatsby-remark-relative-images@1.1.1 31 | 32 | # Install original (a bit hacky but have previously worked for most) 33 | npm i gatsby-remark-relative-images@0.3.0 34 | npm i gatsby-remark-relative-images@0.2.0 35 | ``` 36 | 37 | ## Usage Example 38 | 39 | This usage example is for v2 of this plugin. 40 | 41 | /gatsby-config.js 42 | 43 | ```javascript 44 | module.exports = { 45 | plugins: [ 46 | // Add static assets before markdown files 47 | { 48 | resolve: 'gatsby-source-filesystem', 49 | options: { 50 | path: `${__dirname}/static/uploads`, 51 | name: 'uploads', 52 | }, 53 | }, 54 | { 55 | resolve: 'gatsby-source-filesystem', 56 | options: { 57 | path: `${__dirname}/src/pages`, 58 | name: 'pages', 59 | }, 60 | }, 61 | { 62 | resolve: `gatsby-transformer-remark`, 63 | options: { 64 | plugins: [ 65 | // gatsby-remark-relative-images must go before gatsby-remark-images 66 | { 67 | resolve: `gatsby-remark-relative-images`, 68 | options: { 69 | // [Optional] The root of "media_folder" in your config.yml 70 | // Defaults to "static" 71 | staticFolderName: 'static', 72 | // [Optional] Include the following fields, use dot notation for nested fields 73 | // All fields are included by default 74 | include: ['featured'], 75 | // [Optional] Exclude the following fields, use dot notation for nested fields 76 | // No fields are excluded by default 77 | exclude: ['featured.skip'], 78 | }, 79 | }, 80 | { 81 | resolve: `gatsby-remark-images`, 82 | options: { maxWidth: 1024 }, 83 | }, 84 | ], 85 | }, 86 | }, 87 | ], 88 | }; 89 | ``` 90 | 91 | /static/admin/config.yml 92 | 93 | ```yml 94 | # ... 95 | media_folder: static/img 96 | public_folder: /img 97 | # ... 98 | ``` 99 | 100 | /src/pages/blog-post.md 101 | 102 | ```md 103 | --- 104 | templateKey: blog-post 105 | title: A beginners’ guide to brewing with Chemex 106 | date: 2017-01-04T15:04:10.000Z 107 | featured: { image: /img/chémex.jpg, skip: /img/chémex.jpg } 108 | 109 | description: Brewing with a Chemex probably seems like a complicated, time-consuming ordeal, but once you get used to the process, it becomes a soothing ritual that's worth the effort every time. 110 | --- 111 | 112 | ![chemex](/img/chémex.jpg) 113 | 114 | 115 | 116 | This week we’ll **take** a look at all the steps required to make astonishing coffee with a Chemex at home. The Chemex Coffeemaker is a manual, pour-over style glass-container coffeemaker that Peter Schlumbohm invented in 1941, and which continues to be manufactured by the Chemex Corporation in Chicopee, Massachusetts. 117 | 118 | In 1958, designers at the [Illinois Institute of Technology](https://www.spacefarm.digital) said that the Chemex Coffeemaker is _"one of the best-designed products of modern times"_, and so is included in the collection of the Museum of Modern Art in New York City. 119 | 120 | ## The little secrets of Chemex brewing 121 | 122 | 123 | 124 | 125 | The Chemex Coffeemaker consists of an hourglass-shaped glass flask with a conical funnel-like neck (rather than the cylindrical neck of an Erlenmeyer flask) and uses proprietary filters, made of bonded paper (thicker-gauge paper than the standard paper filters for a drip-method coffeemaker) that removes most of the coffee oils, brewing coffee with a taste that is different than coffee brewed in other coffee-making systems; also, the thicker paper of the Chemex coffee filters may assist in removing cafestol, a cholesterol-containing compound found in coffee oils. 126 | ``` 127 | 128 | ## FAQs 129 | 130 | ### I'm getting the error: Field "image" must not have a selection since type "String" has no subfields 131 | 132 | This is a common error when working with Netlify CMS (see issue [gatsby/gatsby#5990](https://github.com/gatsbyjs/gatsby/issues/5990)). 133 | 134 | The application must include the `media` with `gatsby-source-filesystem` to include all the uploaded media and to make it available on build time. **Note:** The media folder must be included **before** the other content. 135 | 136 | For example, an application that is using NetlifyCMS and this plugin, and has a content folder with markdown that comes from Netlify. Here's how the `gatsby-config.js` should look like: 137 | 138 | ```js 139 | module.exports = { 140 | plugins: [ 141 | { 142 | resolve: `gatsby-source-filesystem`, 143 | options: { 144 | path: `${__dirname}/static/assets`, 145 | name: 'assets', 146 | }, 147 | }, 148 | { 149 | resolve: `gatsby-source-filesystem`, 150 | options: { 151 | path: `${__dirname}/src/content`, 152 | name: 'content', 153 | }, 154 | }, 155 | `gatsby-transformer-sharp`, 156 | `gatsby-plugin-sharp`, 157 | { 158 | resolve: `gatsby-transformer-remark`, 159 | options: { 160 | plugins: [ 161 | `gatsby-remark-relative-images`, 162 | { 163 | resolve: `gatsby-remark-images`, 164 | options: {}, 165 | }, 166 | ], 167 | }, 168 | }, 169 | `gatsby-plugin-netlify-cms`, 170 | ], 171 | }; 172 | ``` 173 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | exports.onCreateNode = require('./dist/on-create-node').onCreateNode; 2 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-remark-relative-images", 3 | "version": "2.0.3", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "gatsby-remark-relative-images", 9 | "version": "2.0.3", 10 | "license": "0BSD", 11 | "dependencies": { 12 | "cheerio": "^1.0.0-rc.12", 13 | "is-invalid-path": "^1.0.2", 14 | "lodash": "^4.17.21", 15 | "traverse": "^0.6.6", 16 | "unist-util-select": "^3.0.4" 17 | }, 18 | "devDependencies": { 19 | "@types/cheerio": "^0.22.21", 20 | "@types/lodash": "^4.14.160", 21 | "@types/node": "^14.6.0", 22 | "@types/slash": "^3.0.0", 23 | "@types/traverse": "^0.6.32", 24 | "@types/unist": "^2.0.3", 25 | "rimraf": "^3.0.2", 26 | "typescript": "^4.0.2" 27 | } 28 | }, 29 | "node_modules/@types/cheerio": { 30 | "version": "0.22.31", 31 | "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.31.tgz", 32 | "integrity": "sha512-Kt7Cdjjdi2XWSfrZ53v4Of0wG3ZcmaegFXjMmz9tfNrZSkzzo36G0AL1YqSdcIA78Etjt6E609pt5h1xnQkPUw==", 33 | "dev": true, 34 | "dependencies": { 35 | "@types/node": "*" 36 | } 37 | }, 38 | "node_modules/@types/lodash": { 39 | "version": "4.14.194", 40 | "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.194.tgz", 41 | "integrity": "sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==", 42 | "dev": true 43 | }, 44 | "node_modules/@types/node": { 45 | "version": "14.18.45", 46 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.45.tgz", 47 | "integrity": "sha512-Nd+FPp60jEaJpm4LAxuLT3wIhB4k0Jdj9DAP4ydqGyMg8DhE+7oM1we+QkwOkpMySTjcqcNfPOWY5kBuAOhkeg==", 48 | "dev": true 49 | }, 50 | "node_modules/@types/slash": { 51 | "version": "3.0.0", 52 | "resolved": "https://registry.npmjs.org/@types/slash/-/slash-3.0.0.tgz", 53 | "integrity": "sha512-bmUaw0IUPUVldtj4YwU7tbzxllQQgsWdnB45bwTI0f1Lq2Yg8lT2yxV4OGZrMTrP/G9v8eVIhX130xPe/RfPfw==", 54 | "deprecated": "This is a stub types definition. slash provides its own type definitions, so you do not need this installed.", 55 | "dev": true, 56 | "dependencies": { 57 | "slash": "*" 58 | } 59 | }, 60 | "node_modules/@types/traverse": { 61 | "version": "0.6.32", 62 | "resolved": "https://registry.npmjs.org/@types/traverse/-/traverse-0.6.32.tgz", 63 | "integrity": "sha512-RBz2uRZVCXuMg93WD//aTS5B120QlT4lR/gL+935QtGsKHLS6sCtZBaKfWjIfk7ZXv/r8mtGbwjVIee6/3XTow==", 64 | "dev": true 65 | }, 66 | "node_modules/@types/unist": { 67 | "version": "2.0.6", 68 | "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", 69 | "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", 70 | "dev": true 71 | }, 72 | "node_modules/balanced-match": { 73 | "version": "1.0.2", 74 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 75 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 76 | "dev": true 77 | }, 78 | "node_modules/boolbase": { 79 | "version": "1.0.0", 80 | "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", 81 | "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" 82 | }, 83 | "node_modules/brace-expansion": { 84 | "version": "1.1.11", 85 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 86 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 87 | "dev": true, 88 | "dependencies": { 89 | "balanced-match": "^1.0.0", 90 | "concat-map": "0.0.1" 91 | } 92 | }, 93 | "node_modules/cheerio": { 94 | "version": "1.0.0-rc.12", 95 | "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", 96 | "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", 97 | "dependencies": { 98 | "cheerio-select": "^2.1.0", 99 | "dom-serializer": "^2.0.0", 100 | "domhandler": "^5.0.3", 101 | "domutils": "^3.0.1", 102 | "htmlparser2": "^8.0.1", 103 | "parse5": "^7.0.0", 104 | "parse5-htmlparser2-tree-adapter": "^7.0.0" 105 | }, 106 | "engines": { 107 | "node": ">= 6" 108 | }, 109 | "funding": { 110 | "url": "https://github.com/cheeriojs/cheerio?sponsor=1" 111 | } 112 | }, 113 | "node_modules/cheerio-select": { 114 | "version": "2.1.0", 115 | "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", 116 | "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", 117 | "dependencies": { 118 | "boolbase": "^1.0.0", 119 | "css-select": "^5.1.0", 120 | "css-what": "^6.1.0", 121 | "domelementtype": "^2.3.0", 122 | "domhandler": "^5.0.3", 123 | "domutils": "^3.0.1" 124 | }, 125 | "funding": { 126 | "url": "https://github.com/sponsors/fb55" 127 | } 128 | }, 129 | "node_modules/concat-map": { 130 | "version": "0.0.1", 131 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 132 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 133 | "dev": true 134 | }, 135 | "node_modules/css-select": { 136 | "version": "5.1.0", 137 | "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", 138 | "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", 139 | "dependencies": { 140 | "boolbase": "^1.0.0", 141 | "css-what": "^6.1.0", 142 | "domhandler": "^5.0.2", 143 | "domutils": "^3.0.1", 144 | "nth-check": "^2.0.1" 145 | }, 146 | "funding": { 147 | "url": "https://github.com/sponsors/fb55" 148 | } 149 | }, 150 | "node_modules/css-selector-parser": { 151 | "version": "1.4.1", 152 | "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-1.4.1.tgz", 153 | "integrity": "sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g==" 154 | }, 155 | "node_modules/css-what": { 156 | "version": "6.1.0", 157 | "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", 158 | "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", 159 | "engines": { 160 | "node": ">= 6" 161 | }, 162 | "funding": { 163 | "url": "https://github.com/sponsors/fb55" 164 | } 165 | }, 166 | "node_modules/dom-serializer": { 167 | "version": "2.0.0", 168 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", 169 | "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", 170 | "dependencies": { 171 | "domelementtype": "^2.3.0", 172 | "domhandler": "^5.0.2", 173 | "entities": "^4.2.0" 174 | }, 175 | "funding": { 176 | "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" 177 | } 178 | }, 179 | "node_modules/domelementtype": { 180 | "version": "2.3.0", 181 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", 182 | "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", 183 | "funding": [ 184 | { 185 | "type": "github", 186 | "url": "https://github.com/sponsors/fb55" 187 | } 188 | ] 189 | }, 190 | "node_modules/domhandler": { 191 | "version": "5.0.3", 192 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", 193 | "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", 194 | "dependencies": { 195 | "domelementtype": "^2.3.0" 196 | }, 197 | "engines": { 198 | "node": ">= 4" 199 | }, 200 | "funding": { 201 | "url": "https://github.com/fb55/domhandler?sponsor=1" 202 | } 203 | }, 204 | "node_modules/domutils": { 205 | "version": "3.1.0", 206 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", 207 | "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", 208 | "dependencies": { 209 | "dom-serializer": "^2.0.0", 210 | "domelementtype": "^2.3.0", 211 | "domhandler": "^5.0.3" 212 | }, 213 | "funding": { 214 | "url": "https://github.com/fb55/domutils?sponsor=1" 215 | } 216 | }, 217 | "node_modules/entities": { 218 | "version": "4.5.0", 219 | "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", 220 | "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", 221 | "engines": { 222 | "node": ">=0.12" 223 | }, 224 | "funding": { 225 | "url": "https://github.com/fb55/entities?sponsor=1" 226 | } 227 | }, 228 | "node_modules/fs.realpath": { 229 | "version": "1.0.0", 230 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 231 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 232 | "dev": true 233 | }, 234 | "node_modules/glob": { 235 | "version": "7.2.3", 236 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", 237 | "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", 238 | "dev": true, 239 | "dependencies": { 240 | "fs.realpath": "^1.0.0", 241 | "inflight": "^1.0.4", 242 | "inherits": "2", 243 | "minimatch": "^3.1.1", 244 | "once": "^1.3.0", 245 | "path-is-absolute": "^1.0.0" 246 | }, 247 | "engines": { 248 | "node": "*" 249 | }, 250 | "funding": { 251 | "url": "https://github.com/sponsors/isaacs" 252 | } 253 | }, 254 | "node_modules/htmlparser2": { 255 | "version": "8.0.2", 256 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", 257 | "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", 258 | "funding": [ 259 | "https://github.com/fb55/htmlparser2?sponsor=1", 260 | { 261 | "type": "github", 262 | "url": "https://github.com/sponsors/fb55" 263 | } 264 | ], 265 | "dependencies": { 266 | "domelementtype": "^2.3.0", 267 | "domhandler": "^5.0.3", 268 | "domutils": "^3.0.1", 269 | "entities": "^4.4.0" 270 | } 271 | }, 272 | "node_modules/inflight": { 273 | "version": "1.0.6", 274 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 275 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 276 | "dev": true, 277 | "dependencies": { 278 | "once": "^1.3.0", 279 | "wrappy": "1" 280 | } 281 | }, 282 | "node_modules/inherits": { 283 | "version": "2.0.4", 284 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 285 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 286 | "dev": true 287 | }, 288 | "node_modules/is-invalid-path": { 289 | "version": "1.0.2", 290 | "resolved": "https://registry.npmjs.org/is-invalid-path/-/is-invalid-path-1.0.2.tgz", 291 | "integrity": "sha512-6KLcFrPCEP3AFXMfnWrIFkZpYNBVzZAoBJJDEZKtI3LXkaDjM3uFMJQjxiizUuZTZ9Oh9FNv/soXbx5TcpaDmA==", 292 | "engines": { 293 | "node": ">=6.0" 294 | } 295 | }, 296 | "node_modules/lodash": { 297 | "version": "4.17.21", 298 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 299 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 300 | }, 301 | "node_modules/minimatch": { 302 | "version": "3.1.2", 303 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 304 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 305 | "dev": true, 306 | "dependencies": { 307 | "brace-expansion": "^1.1.7" 308 | }, 309 | "engines": { 310 | "node": "*" 311 | } 312 | }, 313 | "node_modules/not": { 314 | "version": "0.1.0", 315 | "resolved": "https://registry.npmjs.org/not/-/not-0.1.0.tgz", 316 | "integrity": "sha512-5PDmaAsVfnWUgTUbJ3ERwn7u79Z0dYxN9ErxCpVJJqe2RK0PJ3z+iFUxuqjwtlDDegXvtWoxD/3Fzxox7tFGWA==" 317 | }, 318 | "node_modules/nth-check": { 319 | "version": "2.1.1", 320 | "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", 321 | "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", 322 | "dependencies": { 323 | "boolbase": "^1.0.0" 324 | }, 325 | "funding": { 326 | "url": "https://github.com/fb55/nth-check?sponsor=1" 327 | } 328 | }, 329 | "node_modules/once": { 330 | "version": "1.4.0", 331 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 332 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 333 | "dev": true, 334 | "dependencies": { 335 | "wrappy": "1" 336 | } 337 | }, 338 | "node_modules/parse5": { 339 | "version": "7.1.2", 340 | "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", 341 | "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", 342 | "dependencies": { 343 | "entities": "^4.4.0" 344 | }, 345 | "funding": { 346 | "url": "https://github.com/inikulin/parse5?sponsor=1" 347 | } 348 | }, 349 | "node_modules/parse5-htmlparser2-tree-adapter": { 350 | "version": "7.0.0", 351 | "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", 352 | "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", 353 | "dependencies": { 354 | "domhandler": "^5.0.2", 355 | "parse5": "^7.0.0" 356 | }, 357 | "funding": { 358 | "url": "https://github.com/inikulin/parse5?sponsor=1" 359 | } 360 | }, 361 | "node_modules/path-is-absolute": { 362 | "version": "1.0.1", 363 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 364 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 365 | "dev": true, 366 | "engines": { 367 | "node": ">=0.10.0" 368 | } 369 | }, 370 | "node_modules/rimraf": { 371 | "version": "3.0.2", 372 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", 373 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 374 | "dev": true, 375 | "dependencies": { 376 | "glob": "^7.1.3" 377 | }, 378 | "bin": { 379 | "rimraf": "bin.js" 380 | }, 381 | "funding": { 382 | "url": "https://github.com/sponsors/isaacs" 383 | } 384 | }, 385 | "node_modules/slash": { 386 | "version": "5.0.1", 387 | "resolved": "https://registry.npmjs.org/slash/-/slash-5.0.1.tgz", 388 | "integrity": "sha512-ywNzUOiXwetmLvTUiCBZpLi+vxqN3i+zDqjs2HHfUSV3wN4UJxVVKWrS1JZDeiJIeBFNgB5pmioC2g0IUTL+rQ==", 389 | "dev": true, 390 | "engines": { 391 | "node": ">=14.16" 392 | }, 393 | "funding": { 394 | "url": "https://github.com/sponsors/sindresorhus" 395 | } 396 | }, 397 | "node_modules/traverse": { 398 | "version": "0.6.7", 399 | "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.7.tgz", 400 | "integrity": "sha512-/y956gpUo9ZNCb99YjxG7OaslxZWHfCHAUUfshwqOXmxUIvqLjVO581BT+gM59+QV9tFe6/CGG53tsA1Y7RSdg==", 401 | "funding": { 402 | "url": "https://github.com/sponsors/ljharb" 403 | } 404 | }, 405 | "node_modules/typescript": { 406 | "version": "4.9.5", 407 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", 408 | "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", 409 | "dev": true, 410 | "bin": { 411 | "tsc": "bin/tsc", 412 | "tsserver": "bin/tsserver" 413 | }, 414 | "engines": { 415 | "node": ">=4.2.0" 416 | } 417 | }, 418 | "node_modules/unist-util-is": { 419 | "version": "4.1.0", 420 | "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", 421 | "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", 422 | "funding": { 423 | "type": "opencollective", 424 | "url": "https://opencollective.com/unified" 425 | } 426 | }, 427 | "node_modules/unist-util-select": { 428 | "version": "3.0.4", 429 | "resolved": "https://registry.npmjs.org/unist-util-select/-/unist-util-select-3.0.4.tgz", 430 | "integrity": "sha512-xf1zCu4okgPqGLdhCDpRnjwBNyv3EqjiXRUbz2SdK1+qnLMB7uXXajfzuBvvbHoQ+JLyp4AEbFCGndmc6S72sw==", 431 | "dependencies": { 432 | "css-selector-parser": "^1.0.0", 433 | "not": "^0.1.0", 434 | "nth-check": "^2.0.0", 435 | "unist-util-is": "^4.0.0", 436 | "zwitch": "^1.0.0" 437 | }, 438 | "funding": { 439 | "type": "opencollective", 440 | "url": "https://opencollective.com/unified" 441 | } 442 | }, 443 | "node_modules/wrappy": { 444 | "version": "1.0.2", 445 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 446 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 447 | "dev": true 448 | }, 449 | "node_modules/zwitch": { 450 | "version": "1.0.5", 451 | "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz", 452 | "integrity": "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==", 453 | "funding": { 454 | "type": "github", 455 | "url": "https://github.com/sponsors/wooorm" 456 | } 457 | } 458 | } 459 | } 460 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-remark-relative-images", 3 | "version": "2.0.5", 4 | "description": "Convert image src(s) in markdown to be relative to their node's parent directory. This will help gatsby-remark-images match images outside the node folder. For example, use with NetlifyCMS.", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist", 8 | "*.js" 9 | ], 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "build": "npm run clean && tsc -d", 13 | "watch": "npm run clean && tsc --watch", 14 | "clean": "rimraf ./dist", 15 | "prepare": "npm run build" 16 | }, 17 | "keywords": [ 18 | "gatsby", 19 | "gatsby-plugin", 20 | "image", 21 | "markdown", 22 | "relative", 23 | "netlifycms", 24 | "remark", 25 | "responsive images" 26 | ], 27 | "author": "Daniel Mahon ", 28 | "license": "0BSD", 29 | "dependencies": { 30 | "cheerio": "^1.0.0-rc.12", 31 | "is-invalid-path": "^1.0.2", 32 | "lodash": "^4.17.21", 33 | "traverse": "^0.6.6", 34 | "unist-util-select": "^3.0.4" 35 | }, 36 | "devDependencies": { 37 | "@types/cheerio": "^0.22.21", 38 | "@types/lodash": "^4.14.160", 39 | "@types/node": "^14.6.0", 40 | "@types/slash": "^3.0.0", 41 | "@types/traverse": "^0.6.32", 42 | "@types/unist": "^2.0.3", 43 | "rimraf": "^3.0.2", 44 | "typescript": "^4.0.2" 45 | }, 46 | "repository": { 47 | "type": "git", 48 | "url": "git+https://github.com/danielmahon/gatsby-remark-relative-images.git" 49 | }, 50 | "bugs": { 51 | "url": "https://github.com/danielmahon/gatsby-remark-relative-images/issues" 52 | }, 53 | "homepage": "https://github.com/danielmahon/gatsby-remark-relative-images#readme" 54 | } 55 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { selectAll } from 'unist-util-select'; 3 | import { defaults, isString, find } from 'lodash'; 4 | import cheerio from 'cheerio'; 5 | import { slash } from './utils'; 6 | 7 | export type GatsbyNodePluginArgs = { 8 | files: GatsbyFile[]; 9 | markdownNode: MarkdownNode; 10 | markdownAST: any; 11 | reporter: { 12 | info: (msg: string, error?: Error) => void; 13 | }; 14 | }; 15 | 16 | export type GatsbyFile = { 17 | absolutePath: string; 18 | }; 19 | 20 | export type PluginOptions = { 21 | staticFolderName: string; 22 | include: string[]; 23 | exclude: string[]; 24 | }; 25 | 26 | export type FrontMatterOptions = { 27 | staticFolderName: string; 28 | include: string[]; 29 | exclude: string[]; 30 | }; 31 | 32 | export type MarkdownNode = { 33 | id: string; 34 | parent: string; 35 | url: string; 36 | frontmatter?: object; 37 | internal: { 38 | type: string; 39 | }; 40 | fileAbsolutePath: string; 41 | }; 42 | 43 | export type Node = { 44 | dir: string; 45 | }; 46 | 47 | export type HtmlNode = { 48 | value: string; 49 | } & MarkdownNode; 50 | 51 | export const defaultPluginOptions = { 52 | staticFolderName: 'static', 53 | include: [], 54 | exclude: [], 55 | }; 56 | 57 | export const findMatchingFile = ( 58 | src: string, 59 | files: GatsbyFile[], 60 | options: PluginOptions 61 | ) => { 62 | const result = find(files, (file) => { 63 | const staticPath = slash(path.join(options.staticFolderName, src)); 64 | return slash(path.normalize(file.absolutePath)).endsWith(staticPath); 65 | }); 66 | if (!result) { 67 | throw new Error( 68 | `No matching file found for src "${src}" in static folder "${options.staticFolderName}". Please check static folder name and that file exists at "${options.staticFolderName}${src}". This error will probably cause a "GraphQLDocumentError" later in build. All converted field paths MUST resolve to a matching file in the "static" folder.` 69 | ); 70 | } 71 | return result; 72 | }; 73 | 74 | export default async ( 75 | { files, markdownNode, markdownAST }: GatsbyNodePluginArgs, 76 | pluginOptions: PluginOptions 77 | ) => { 78 | // Default options 79 | const options = defaults(pluginOptions, defaultPluginOptions); 80 | 81 | if (!markdownNode.fileAbsolutePath) return; 82 | 83 | const directory = path.dirname(markdownNode.fileAbsolutePath); 84 | 85 | // Process all markdown image nodes 86 | selectAll('image', markdownAST).forEach((_node: any) => { 87 | const node = _node as MarkdownNode; 88 | if (!isString(node.url)) return; 89 | if (!path.isAbsolute(node.url) || !path.extname(node.url)) return; 90 | 91 | const file = findMatchingFile(node.url, files, options); 92 | 93 | // Update node.url to be relative to its parent file 94 | node.url = path.relative(directory, file.absolutePath); 95 | }); 96 | 97 | // Process all HTML images in markdown body 98 | selectAll('html', markdownAST).forEach((_node: any) => { 99 | const node = _node as HtmlNode; 100 | 101 | const $ = cheerio.load(node.value); 102 | 103 | if ($(`img`).length === 0) return; 104 | 105 | $(`img`).each((_, element) => { 106 | // Get the details we need. 107 | const url = $(element).attr(`src`); 108 | 109 | // Only handle absolute (local) urls 110 | if (!isString(url)) return; 111 | if (!path.isAbsolute(url) || !path.extname(url)) return; 112 | 113 | const file = findMatchingFile(url, files, options); 114 | 115 | // Make the image src relative to its parent node 116 | const src = path.relative(directory, file.absolutePath); 117 | $(element).attr('src', src); 118 | 119 | node.value = $(`body`).html() ?? ''; // fix for cheerio v1 120 | }); 121 | }); 122 | }; 123 | -------------------------------------------------------------------------------- /src/on-create-node.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defaults, isString } from 'lodash'; 3 | import traverse from 'traverse'; 4 | import { 5 | defaultPluginOptions, 6 | PluginOptions, 7 | GatsbyFile, 8 | MarkdownNode, 9 | findMatchingFile, 10 | } from '.'; 11 | import { slash } from './utils'; 12 | 13 | export type GatsbyPluginArgs = { 14 | node: MarkdownNode; 15 | getNodesByType: (type: string) => GatsbyFile[]; 16 | reporter: { 17 | info: (msg: string, error?: Error) => void; 18 | }; 19 | }; 20 | 21 | export const onCreateNode = ( 22 | { node, getNodesByType }: GatsbyPluginArgs, 23 | pluginOptions: PluginOptions 24 | ) => { 25 | const options = defaults(pluginOptions, defaultPluginOptions); 26 | 27 | if (node.fileAbsolutePath && node.internal.type === `MarkdownRemark` || node.internal.type === `Mdx`) { 28 | const files = getNodesByType(`File`); 29 | 30 | const directory = path.dirname(node.fileAbsolutePath); 31 | 32 | // Deeply iterate through frontmatter data for absolute paths 33 | traverse(node.frontmatter).forEach(function (value) { 34 | if (!isString(value)) return; 35 | if (!path.isAbsolute(value) || !path.extname(value)) return; 36 | 37 | const paths = this.path.reduce((acc, current) => { 38 | acc.push(acc.length > 0 ? [acc, current].join('.') : current); 39 | return acc; 40 | }, []); 41 | 42 | let shouldTransform = options.include.length < 1; 43 | 44 | if (options.include.some((a) => paths.includes(a))) { 45 | shouldTransform = true; 46 | } 47 | 48 | if (options.exclude.some((a) => paths.includes(a))) { 49 | shouldTransform = false; 50 | } 51 | 52 | if (!shouldTransform) return; 53 | 54 | const file = findMatchingFile(value, files, options); 55 | 56 | const newValue = slash(path.relative(directory, file.absolutePath)); 57 | 58 | this.update(newValue); 59 | }); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export const slash = (path: string): string => { 2 | const isExtendedLengthPath = /^\\\\\?\\/.test(path); 3 | 4 | if (isExtendedLengthPath) { 5 | return path; 6 | } 7 | return path.replace(/\\/g, `/`); 8 | }; 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*"], 3 | "compilerOptions": { 4 | "target": "es6", 5 | "module": "commonjs", 6 | "noImplicitAny": false, 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "outDir": "dist" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'is-invalid-path'; 2 | --------------------------------------------------------------------------------