├── .nvmrc ├── .prettierignore ├── .eslintrc ├── .npmignore ├── .github └── workflows │ └── ci.yml ├── CHANGELOG.md ├── .gitignore ├── package.json ├── LICENSE ├── dist ├── markdown-it-link-attributes.min.js └── markdown-it-link-attributes.js ├── index.js ├── README.md ├── examples └── index.html └── test └── index.test.js /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["prettier"] 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | tasks 3 | examples 4 | bower.json 5 | .mversionrc 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "Unit Tests" 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | name: "Unit Tests on Ubuntu" 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Use Node.js 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version-file: ".nvmrc" 16 | - run: npm install 17 | - run: npm test 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # unreleased 2 | 3 | _Breaking Changes_ 4 | 5 | - Drop support for ES5 browsers 6 | - Drop support for Node < 18 7 | 8 | # 4.0.1 9 | 10 | - Fix documentation error in README 11 | 12 | # 4.0.0 13 | 14 | - Add `matcher` function configuration 15 | 16 | _Breaking Changes_ 17 | 18 | - Support latest major version of Markdown-it (currently 12) 19 | - Drop Node < v14 20 | - Drop `pattern` configuration 21 | - Drop Bower support 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdown-it-link-attributes", 3 | "version": "4.0.1", 4 | "description": "A markdown-it plugin to configure the attributes for links", 5 | "main": "index.js", 6 | "scripts": { 7 | "prebuild": "prettier --write .", 8 | "lint": "eslint", 9 | "pretest": "npm run lint", 10 | "test": "jest" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/crookedneighbor/markdown-it-link-attributes" 15 | }, 16 | "keywords": [ 17 | "markdown", 18 | "markdown-it", 19 | "markdown-it-plugin" 20 | ], 21 | "author": "Blade Barringer ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/crookedneighbor/markdown-it-link-attributes" 25 | }, 26 | "homepage": "https://github.com/crookedneighbor/markdown-it-link-attributes", 27 | "devDependencies": { 28 | "eslint": "^8.51.0", 29 | "eslint-config-prettier": "^9.0.0", 30 | "jest": "^29.7.0", 31 | "markdown-it": "latest", 32 | "prettier": "^3.0.3" 33 | }, 34 | "eslint": { 35 | "ignore": [ 36 | "dist/" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Blade Barringer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dist/markdown-it-link-attributes.min.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.markdownitLinkAttributes=f()}})(function(){var define,module,exports;return function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i Link attributes plugin for [markdown-it](https://github.com/markdown-it/markdown-it) markdown parser. 4 | 5 | ## Install 6 | 7 | ```bash 8 | npm install markdown-it-link-attributes --save 9 | ``` 10 | 11 | ## Use 12 | 13 | ### Basic Configuration 14 | 15 | You can pass an object with an attrs property. Each link parsed with this config will have the passed attributes. 16 | 17 | ```js 18 | const md = require("markdown-it")(); 19 | const mila = require("markdown-it-link-attributes"); 20 | 21 | md.use(mila, { 22 | attrs: { 23 | target: "_blank", 24 | rel: "noopener", 25 | }, 26 | }); 27 | 28 | var result = md.render("[Example](https://example.com"); 29 | 30 | result; // Example 31 | ``` 32 | 33 | If the `linkify` option is set to `true` on `markdown-it`, then the attributes will be applied to plain links as well. 34 | 35 | ```js 36 | const md = require("markdown-it")({ 37 | linkify: true, 38 | }); 39 | 40 | md.use(mila, { 41 | target: "_blank", 42 | rel: "noopener", 43 | }); 44 | 45 | var html = md.render("foo https://google.com bar"); 46 | html; //

foo https://google.com bar

47 | ``` 48 | 49 | ### Applying classes 50 | 51 | You can apply a `class` to a link by using a `class` or a `className` property. Either one will work, but use only one, not both. 52 | 53 | ```js 54 | md.use(mila, { 55 | attrs: { 56 | class: "my-class", 57 | }, 58 | }); 59 | 60 | // or 61 | md.use(mila, { 62 | attrs: { 63 | className: "my-class", 64 | }, 65 | }); 66 | ``` 67 | 68 | ### Conditionally apply attributes 69 | 70 | You can choose to test a link's `href` against a matcher function. The attributes will be applied only if the matcher function returns true. 71 | 72 | ```js 73 | md.use(mila, { 74 | matcher(href, config) { 75 | return href.startsWith("https:"); 76 | }, 77 | attrs: { 78 | target: "_blank", 79 | rel: "noopener", 80 | }, 81 | }); 82 | 83 | const matchingResult = md.render("[Matching Example](https://example.com"); 84 | const ignoredResult = md.render("[Not Matching Example](http://example.com"); 85 | 86 | matchingResult; // Matching Example 87 | ignoredResult; // Not Matching Example 88 | ``` 89 | 90 | ### Multiple Configurations 91 | 92 | Alternatively, you can pass an Array of configurations. The first matcher function to return true will be applied to the link. 93 | 94 | ```js 95 | md.use(mila, [ 96 | { 97 | matcher(href) { 98 | return href.match(/^https?:\/\//); 99 | }, 100 | attrs: { 101 | class: "external-link", 102 | }, 103 | }, 104 | { 105 | matcher(href) { 106 | return href.startsWith("/"); 107 | }, 108 | attrs: { 109 | class: "absolute-link", 110 | }, 111 | }, 112 | { 113 | matcher(href) { 114 | return href.startsWith("/blue/"); 115 | }, 116 | attrs: { 117 | class: "link-that-contains-the-word-blue", 118 | }, 119 | }, 120 | ]); 121 | 122 | var externalResult = md.render("[external](https://example.com"); 123 | var absoluteResult = md.render("[absolute](/some-page"); 124 | var blueResult = md.render("[blue](relative/link/with/blue/in/the/name"); 125 | 126 | externalResult; // external 127 | absoluteResult; // absolute 128 | blueResult; // blue 129 | ``` 130 | 131 | If multiple matcher functions return true, the first configuration to match will be used. 132 | 133 | ```js 134 | // This matches both the "starts with http or https" rule and the "contains the word blue" rule. 135 | // Since the http/https rule was defined first, that is the configuration that is used. 136 | var result = md.render("[external](https://example.com/blue"); 137 | 138 | result; // external 139 | ``` 140 | 141 | ## Usage in the browser 142 | 143 | _Differences in browser._ If you load script directly into the page, without a package system, the module will add itself globally as `window.markdownitLinkAttributes`. 144 | You need to load `dist/markdown-it-link-attributes.min.js`, if you don't use a build system. 145 | 146 | ## Testing 147 | 148 | This plugin is tested against the latest version of markdown-it 149 | 150 | ## License 151 | 152 | [MIT](https://github.com/markdown-it/markdown-it-footnote/blob/master/LICENSE) 153 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Markdown-It Link Attributes Plugin 4 | 9 | 13 | 14 | 15 | 16 | 19 | 37 | 38 | 39 |
40 |

Markdown-It Link Attributes

41 |
42 | 43 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var MarkdownIt = require("markdown-it"); 4 | var linkAttributes = require("../"); 5 | 6 | describe("markdown-it-link-attributes", function () { 7 | var md; 8 | 9 | beforeEach(function () { 10 | md = MarkdownIt(); 11 | }); 12 | 13 | it("adds attribues to link", function () { 14 | md.use(linkAttributes, { 15 | attrs: { 16 | target: "_blank", 17 | }, 18 | }); 19 | 20 | var result = md.render("[link](https://google.com)"); 21 | 22 | expect(result).toMatch( 23 | 'link', 24 | ); 25 | }); 26 | 27 | it("can pass in multiple attributes", function () { 28 | md.use(linkAttributes, { 29 | attrs: { 30 | target: "_blank", 31 | rel: "noopener", 32 | foo: "bar", 33 | }, 34 | }); 35 | 36 | var result = md.render("[link](https://google.com)"); 37 | 38 | expect(result).toMatch( 39 | 'link', 40 | ); 41 | }); 42 | 43 | it("takes matcher function if it returns true", function () { 44 | md.use(linkAttributes, { 45 | matcher: function (href) { 46 | return /^https?:\/\//.test(href); 47 | }, 48 | attrs: { 49 | target: "_blank", 50 | rel: "noopener", 51 | }, 52 | }); 53 | 54 | var result = md.render("[link](https://google.com)"); 55 | expect(result).toMatch( 56 | 'link', 57 | ); 58 | 59 | result = md.render("[link](#anchor)"); 60 | expect(result).toMatch('link'); 61 | }); 62 | 63 | it("allows multiple rules", function () { 64 | md.use(linkAttributes, [ 65 | { 66 | matcher: function (href) { 67 | return href.indexOf("https://") === 0; 68 | }, 69 | attrs: { 70 | class: "has-text-uppercase", 71 | }, 72 | }, 73 | { 74 | matcher: function (href) { 75 | return href.indexOf("#") === 0; 76 | }, 77 | attrs: { 78 | class: "is-blue", 79 | }, 80 | }, 81 | { 82 | attrs: { 83 | class: "is-red", 84 | }, 85 | }, 86 | ]); 87 | 88 | var result = md.render("[Google](https://www.google.com)"); 89 | expect(result).toMatch( 90 | 'Google', 91 | ); 92 | 93 | result = md.render("[Go to top](#top)"); 94 | expect(result).toMatch('Go to top'); 95 | 96 | result = md.render("[About](/page/about)"); 97 | expect(result).toMatch('About'); 98 | }); 99 | 100 | it("uses the first rule that matches if multiple match", function () { 101 | md.use(linkAttributes, [ 102 | { 103 | matcher: function (href) { 104 | return href.includes("g"); 105 | }, 106 | attrs: { 107 | class: "contains-g", 108 | }, 109 | }, 110 | { 111 | matcher: function (href) { 112 | return href.indexOf("https://") === 0; 113 | }, 114 | attrs: { 115 | class: "starts-with-https", 116 | }, 117 | }, 118 | { 119 | matcher: function (href) { 120 | return href.indexOf("http") === 0; 121 | }, 122 | attrs: { 123 | class: "starts-with-http", 124 | }, 125 | }, 126 | ]); 127 | 128 | var result = md.render("[Google](https://www.google.com)"); 129 | expect(result).toMatch( 130 | 'Google', 131 | ); 132 | 133 | result = md.render("[Not Google](https://www.example.com)"); 134 | expect(result).toMatch( 135 | 'Not Google', 136 | ); 137 | 138 | result = md.render("[Not Google and not secure](http://www.example.com)"); 139 | expect(result).toMatch( 140 | 'Not Google and not secure', 141 | ); 142 | 143 | result = md.render("[Not Google and not secure](http://www.example.com/g)"); 144 | expect(result).toMatch( 145 | 'Not Google and not secure', 146 | ); 147 | }); 148 | 149 | // NEXT_MAJOR_VERSION we should probably apply all that apply instead of just going with the first to apply 150 | // The problem will be when multiple attrs are modifying the same property, in which case we'll probably just want to go with the first 151 | it("only uses the first rule if the first rule has no matcher", function () { 152 | md.use(linkAttributes, [ 153 | { 154 | attrs: { 155 | class: "always-use-this", 156 | }, 157 | }, 158 | { 159 | matcher: function (href) { 160 | return href.includes("g"); 161 | }, 162 | attrs: { 163 | class: "contains-g", 164 | }, 165 | }, 166 | { 167 | matcher: function (href) { 168 | return href.indexOf("https://") === 0; 169 | }, 170 | attrs: { 171 | class: "starts-with-https", 172 | }, 173 | }, 174 | { 175 | matcher: function (href) { 176 | return href.indexOf("http") === 0; 177 | }, 178 | attrs: { 179 | class: "starts-with-http", 180 | }, 181 | }, 182 | ]); 183 | 184 | var result = md.render("[Google](https://www.google.com)"); 185 | expect(result).toMatch( 186 | 'Google', 187 | ); 188 | 189 | result = md.render("[Not Google](https://www.example.com)"); 190 | expect(result).toMatch( 191 | 'Not Google', 192 | ); 193 | 194 | result = md.render("[Not Google and not secure](http://www.example.com)"); 195 | expect(result).toMatch( 196 | 'Not Google and not secure', 197 | ); 198 | 199 | result = md.render("[Not Google and not secure](http://www.example.com/g)"); 200 | expect(result).toMatch( 201 | 'Not Google and not secure', 202 | ); 203 | }); 204 | 205 | it("treats className as if it is class", function () { 206 | md.use(linkAttributes, { 207 | attrs: { 208 | className: "foo", 209 | }, 210 | }); 211 | 212 | var result = md.render("[Google](https://www.google.com)"); 213 | 214 | expect(result).toMatch('class="foo"'); 215 | }); 216 | 217 | it("retains the original attr of a previous plugin that alters the attrs", function () { 218 | md.use(linkAttributes, { 219 | attrs: { 220 | keep: "keep", 221 | overwrite: "original", 222 | }, 223 | }); 224 | 225 | var original = md.render("[link](https://google.com)"); 226 | 227 | expect(original).toMatch( 228 | 'link', 229 | ); 230 | 231 | md.use(linkAttributes, { 232 | attrs: { 233 | overwrite: "new", 234 | newattr: "new", 235 | }, 236 | }); 237 | 238 | var result = md.render("[link](https://google.com)"); 239 | 240 | expect(result).toMatch( 241 | 'link', 242 | ); 243 | }); 244 | 245 | it("works on plain urls when linkify is set to true", function () { 246 | var md = new MarkdownIt({ 247 | linkify: true, 248 | }); 249 | md.use(linkAttributes, { 250 | attrs: { 251 | target: "_blank", 252 | }, 253 | }); 254 | 255 | var result = md.render("foo https://google.com bar"); 256 | 257 | expect(result).toMatch( 258 | 'https://google.com', 259 | ); 260 | }); 261 | 262 | it("calls link_open function if provided", function () { 263 | var spy = (md.renderer.rules.link_open = jest.fn()); 264 | md.use(linkAttributes); 265 | 266 | md.render("[link](https://google.com)"); 267 | 268 | expect(spy).toBeCalledTimes(1); 269 | }); 270 | 271 | it("calls default render if link_open rule is not defined", function () { 272 | var spy = jest.spyOn(linkAttributes, "defaultRender"); 273 | md.use(linkAttributes); 274 | 275 | md.render("[link](https://google.com)"); 276 | 277 | expect(spy).toBeCalledTimes(1); 278 | }); 279 | }); 280 | --------------------------------------------------------------------------------