├── .eslintignore ├── .prettierignore ├── assets ├── hero.png ├── infima.png └── classic.png ├── .npmignore ├── .gitignore ├── .eslintrc ├── test ├── sample.md ├── none.ref ├── emoji.ref ├── index.js └── svg.ref ├── .travis.yml ├── package.json ├── styles ├── infima.css └── classic.css ├── README.md └── lib └── index.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules/ -------------------------------------------------------------------------------- /assets/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/remark-admonitions/master/assets/hero.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | test 3 | .eslintignore 4 | .eslintrc 5 | .prettierignore 6 | .travis.yml -------------------------------------------------------------------------------- /assets/infima.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/remark-admonitions/master/assets/infima.png -------------------------------------------------------------------------------- /assets/classic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/remark-admonitions/master/assets/classic.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # node modules 2 | node_modules 3 | # files generated during testing 4 | test/*.html 5 | # editor configuration files 6 | .vscode -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "plugins": [ 9 | "prettier" 10 | ], 11 | "extends": ["eslint:recommended", "plugin:prettier/recommended"], 12 | "parserOptions": { 13 | "sourceType": "module", 14 | "ecmaVersion": 2018 15 | }, 16 | "settings": { 17 | }, 18 | "rules": { 19 | "prettier/prettier": ["error"] 20 | } 21 | } -------------------------------------------------------------------------------- /test/sample.md: -------------------------------------------------------------------------------- 1 | # This is a test 2 | :::note 3 | This is what a note looks like 4 | ::: 5 | 6 | :::tip 7 | It works great with docusaurus 2.0 8 | ::: 9 | 10 | :::caution you can set *your own* title 11 | it replaces the default title 12 | ::: 13 | 14 | :::important credit 15 | Based on `remarkable-admonitions` 16 | 17 | SVG Icons by GitHub Octicons 18 | ::: 19 | 20 | :::warning 21 | You can't nest them 22 | * but 23 | * you 24 | * can 25 | * nest 26 | * other 27 | * markdown 28 | 29 | ```javascript 30 | // you can even use block elements 31 | ``` 32 | ::: 33 | 34 | :::custom 35 | You can make your own custom types. The icon, keyword, and emoji can be set in the plugin options and they can be styled separately. 36 | ::: -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | os: linux 5 | jobs: 6 | include: 7 | - stage: test 8 | script: npm test 9 | - stage: lint 10 | script: npm run lint 11 | - stage: release 12 | if: type = push AND tag IS present 13 | install: true 14 | script: skip 15 | deploy: 16 | provider: npm 17 | email: elviswolcott@gmail.com 18 | api_key: 19 | secure: lNF+2FSZU0EsH9jerhWYbZT9dKiO1Kfz2NPuA/mSk5UkVTOQC+8A9ufkyF9kkwcDgBXvxsUcI0OM9C0wQatE2DIqGNOo9YzkUfF7zcxC63Nb878alp6tcVDflRIP/a5X3la5loq7uxEPD/8JJICt54Y6OJBW8GlZ8+ZvtlZzba8ZIW3G6JEBh1mXH1ZGBCbRuVw7kLaFe7E1VEgYHLIcAm6bVB8DvK4Kqtjx4karv8y8+u2NMV+qj9QkrqHpqTfcwNqbQXmKJIcdX42tC4f8ImweeE4Jvv0s6bg76FTuOR1O4yTp+8gUplZqdP6JWJj3OS3RaooYrrUqN4d7ZJ5z8u3yvzu8LrVfQmruza+TkixNLKzQfGwAxPtt/Rc/VWyG3aJNcz8AkAyufnLX/OOT6XR2oBSZtB9Dhk7OcZeKu9ZpLg9J8Z73weWJipCYsVefnxEVg01fzqrfeKBC1jBeuOEe3IEcMkczpxDAp4o5wpyc7KaHdcWTFzasOIWjb/XNgQ6JRylIDD+Zg9HiQjzKodE3hDELMypRHVBD2a9sdxO2pNIpBSvf7LAJAz5okxl1+y2S9Hn6+aNEL0aMMP0pxBi4A28vVc4oSVHy4Uz1mKaJSj1z6BWxZ0N06CKAoDe8GjWi3ycimRvU86nGIUOWJ/eBVNRuqNdFmRW4+zVs6WU= 20 | on: 21 | tags: true 22 | repo: elviswolcott/remark-admonitions 23 | branch: master 24 | stages: 25 | - test 26 | - lint 27 | - release 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remark-admonitions", 3 | "version": "1.2.2", 4 | "description": "Add admonitions support to remark", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "node test/index.js", 8 | "lint": "eslint .", 9 | "lint:fix": "eslint --fix ." 10 | }, 11 | "husky": { 12 | "hooks": { 13 | "pre-commit": "npm run lint:fix" 14 | } 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/elviswolcott/remark-admonitions.git" 19 | }, 20 | "keywords": [ 21 | "remark", 22 | "docusaurus", 23 | "admonitions", 24 | "markdown", 25 | "mdx", 26 | "plugin" 27 | ], 28 | "author": "Elvis Wolcott", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/elviswolcott/remark-admonitions/issues" 32 | }, 33 | "homepage": "https://github.com/elviswolcott/remark-admonitions#readme", 34 | "dependencies": { 35 | "rehype-parse": "^6.0.2", 36 | "unified": "^8.4.2", 37 | "unist-util-visit": "^2.0.1" 38 | }, 39 | "devDependencies": { 40 | "colors": "^1.4.0", 41 | "compiler": "^0.1.2", 42 | "diff": "^4.0.1", 43 | "eslint": "^6.8.0", 44 | "eslint-config-prettier": "^6.9.0", 45 | "eslint-plugin-import": "^2.18.2", 46 | "eslint-plugin-prettier": "^3.1.2", 47 | "husky": "^4.0.6", 48 | "prettier": "^1.19.1", 49 | "rehype-document": "^3.2.1", 50 | "rehype-format": "^3.0.0", 51 | "rehype-stringify": "^6.0.1", 52 | "remark-parse": "^7.0.2", 53 | "remark-rehype": "^5.0.0", 54 | "to-vfile": "^6.0.0", 55 | "vfile-reporter": "^6.0.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /styles/infima.css: -------------------------------------------------------------------------------- 1 | /* base admonitions variables of off infima variables */ 2 | :root { 3 | --ra-admonition-background-color: var(--ifm-alert-background-color); 4 | --ra-admonition-border-width: var(--ifm-alert-border-width); 5 | --ra-admonition-border-color: var(--ifm-alert-border-color); 6 | --ra-admonition-border-radius: var(--ifm-alert-border-radius); 7 | --ra-admonition-color: var(--ifm-alert-color); 8 | --ra-admonition-padding-vertical: var(--ifm-alert-padding-vertical); 9 | --ra-admonition-padding-horizontal: var(--ifm-alert-padding-horizontal); 10 | --ra-color-note: var(--ifm-color-secondary); 11 | --ra-color-important: var(--ifm-color-info); 12 | --ra-color-tip: var(--ifm-color-success); 13 | --ra-color-caution: var(--ifm-color-warning); 14 | --ra-color-warning: var(--ifm-color-danger); 15 | --ra-color-text-dark: var(--ifm-color-gray-900); 16 | } 17 | 18 | /* apply variables to admonitions */ 19 | 20 | .admonition { 21 | margin-bottom: 1em; 22 | } 23 | 24 | .admonition:not(.alert) { 25 | background-color: var(--ra-admonition-background-color); 26 | border: var(--ra-admonition-border-width) solid var(--ra-admonition-border-color); 27 | border-radius: var(--ra-admonition-border-radius); 28 | box-sizing: border-box; 29 | color: var(--ra-admonition-color); 30 | padding: var(--ra-admonition-padding-vertical) var(--ra-admonition-padding-horizontal); 31 | } 32 | 33 | .admonition:not(.alert) { 34 | --ra-admonition-background-color: var(--ifm-color-primary) 35 | } 36 | 37 | .admonition h5 { 38 | margin-top: 0; 39 | margin-bottom: 8px; 40 | text-transform: uppercase; 41 | } 42 | 43 | .admonition-icon { 44 | display: inline-block; 45 | vertical-align: middle; 46 | margin-right: 0.2em; 47 | } 48 | 49 | .admonition-icon svg { 50 | display: inline-block; 51 | width: 22px; 52 | height: 22px; 53 | stroke-width: 0; 54 | fill: var(--ra-admonition-icon-color); 55 | stroke: var(--ra-admonition-icon-color); 56 | } 57 | 58 | .admonition-content > :last-child { 59 | margin-bottom: 0; 60 | } 61 | 62 | 63 | /* set variables based on admonition type */ 64 | 65 | .admonition { 66 | --ra-admonition-icon-color: var(--ra-admonition-color); 67 | } 68 | 69 | .admonition-note { 70 | --ra-admonition-color: var(--ra-color-text-dark); 71 | } -------------------------------------------------------------------------------- /styles/classic.css: -------------------------------------------------------------------------------- 1 | .admonition { 2 | margin-bottom: 1em; 3 | padding: 15px 30px 15px 15px; 4 | } 5 | 6 | .admonition h5 { 7 | margin-top: 0; 8 | margin-bottom: 8px; 9 | text-transform: uppercase; 10 | } 11 | 12 | .admonition-icon { 13 | display: inline-block; 14 | vertical-align: middle; 15 | margin-right: 0.2em; 16 | } 17 | 18 | .admonition-icon svg { 19 | display: inline-block; 20 | width: 22px; 21 | height: 22px; 22 | stroke-width: 0; 23 | } 24 | 25 | .admonition-content > :last-child { 26 | margin-bottom: 0; 27 | } 28 | 29 | /* default for custom types */ 30 | .admonition { 31 | background-color: rgba(118, 51, 219, 0.1); 32 | border-left: 8px solid #7633db; 33 | } 34 | 35 | .admonition h5 { 36 | color: #7633db; 37 | } 38 | 39 | .admonition .admonition-icon svg { 40 | stroke: #7633db; 41 | fill: #7633db; 42 | } 43 | 44 | /** native types */ 45 | .admonition-caution { 46 | background-color: rgba(230, 126, 34, 0.1); 47 | border-left: 8px solid #e67e22; 48 | } 49 | 50 | .admonition-caution h5 { 51 | color: #e67e22; 52 | } 53 | 54 | .admonition-caution .admonition-icon svg { 55 | stroke: #e67e22; 56 | fill: #e67e22; 57 | } 58 | 59 | .admonition-tip { 60 | background-color: rgba(46, 204, 113, 0.1); 61 | border-left: 8px solid #2ecc71; 62 | } 63 | 64 | .admonition-tip h5 { 65 | color: #2ecc71; 66 | } 67 | 68 | .admonition-tip .admonition-icon svg { 69 | stroke: #2ecc71; 70 | fill: #2ecc71; 71 | } 72 | 73 | .admonition-warning { 74 | background-color: rgba(231, 76, 60, 0.1); 75 | border-left: 8px solid #e74c3c; 76 | } 77 | 78 | .admonition-warning h5 { 79 | color: #e74c3c; 80 | } 81 | 82 | .admonition-warning .admonition-icon svg { 83 | stroke: #e74c3c; 84 | fill: #e74c3c; 85 | } 86 | 87 | .admonition-important { 88 | background-color: rgba(52, 152, 219, 0.1); 89 | border-left: 8px solid #3498db; 90 | } 91 | 92 | .admonition-important h5 { 93 | color: #3498db; 94 | } 95 | 96 | .admonition-important .admonition-icon svg { 97 | stroke: #3498db; 98 | fill: #3498db; 99 | } 100 | 101 | .admonition-note { 102 | background-color: rgba(241, 196, 15, 0.1); 103 | border-left: 8px solid #f1c40f; 104 | } 105 | 106 | .admonition-note h5 { 107 | color: #f1c40f; 108 | } 109 | 110 | .admonition-note .admonition-icon svg { 111 | stroke: #f1c40f; 112 | fill: #f1c40f; 113 | } -------------------------------------------------------------------------------- /test/none.ref: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | sample 6 | 7 | 8 | 9 |

This is a test

10 |
11 |
12 |
Note
13 |
14 |
15 |

This is what a note looks like

16 |
17 |
18 |
19 |
20 |
Tip
21 |
22 |
23 |

It works great with docusaurus 2.0

24 |
25 |
26 |
27 |
28 |
you can set your own title
29 |
30 |
31 |

it replaces the default title

32 |
33 |
34 |
35 |
36 |
credit
37 |
38 |
39 |

Based on remarkable-admonitions

40 |

SVG Icons by GitHub Octicons

41 |
42 |
43 |
44 |
45 |
Warning
46 |
47 |
48 |

You can't nest them

49 | 57 |
// you can even use block elements
58 | 
59 |
60 |
61 |
62 |
63 |
Custom
64 |
65 |
66 |

You can make your own custom types. The icon, keyword, and emoji can be set in the plugin options and they can be styled separately.

67 |
68 |
69 | 70 | 71 | -------------------------------------------------------------------------------- /test/emoji.ref: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | sample 6 | 7 | 8 | 9 |

This is a test

10 |
11 |
12 |
ℹ️Note
13 |
14 |
15 |

This is what a note looks like

16 |
17 |
18 |
19 |
20 |
💡Tip
21 |
22 |
23 |

It works great with docusaurus 2.0

24 |
25 |
26 |
27 |
28 |
⚠️you can set your own title
29 |
30 |
31 |

it replaces the default title

32 |
33 |
34 |
35 |
36 |
❗️credit
37 |
38 |
39 |

Based on remarkable-admonitions

40 |

SVG Icons by GitHub Octicons

41 |
42 |
43 |
44 |
45 |
🔥Warning
46 |
47 |
48 |

You can't nest them

49 | 57 |
// you can even use block elements
58 | 
59 |
60 |
61 |
62 |
63 |
💻Custom
64 |
65 |
66 |

You can make your own custom types. The icon, keyword, and emoji can be set in the plugin options and they can be styled separately.

67 |
68 |
69 | 70 | 71 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const unified = require("unified"); 2 | const markdown = require("remark-parse"); 3 | const plugin = require("../lib"); 4 | const remark2rehype = require("remark-rehype"); 5 | const doc = require("rehype-document"); 6 | const format = require("rehype-format"); 7 | const html = require("rehype-stringify"); 8 | const vfile = require("to-vfile"); 9 | const report = require("vfile-reporter"); 10 | const diff = require("diff"); 11 | const colors = require("colors/safe"); 12 | 13 | let exit = 0; 14 | // naive diff, works fine for short files 15 | const diffVfile = (a, b) => { 16 | if (a.toString() !== b.toString()) { 17 | const changes = diff.diffLines(a.toString(), b.toString()); 18 | const pretty = changes 19 | .map(group => { 20 | let text = group.value; 21 | if (group.added) { 22 | return text 23 | .trim() 24 | .split("\n") 25 | .map(line => `+ |${colors.green(line)}`) 26 | .join("\n"); 27 | } else if (group.removed) { 28 | return text 29 | .trim() 30 | .split("\n") 31 | .map(line => `- |${colors.red(line)}`) 32 | .join("\n"); 33 | } else { 34 | return text 35 | .trim() 36 | .split("\n") 37 | .map(line => ` |${line}`) 38 | .join("\n"); 39 | } 40 | }) 41 | .join("\n"); 42 | return { same: true, pretty }; 43 | } else { 44 | return { same: false, pretty: "" }; 45 | } 46 | }; 47 | 48 | const test = (options, filename) => { 49 | unified() 50 | .use(markdown) 51 | .use(plugin, options) 52 | .use(remark2rehype) 53 | .use(doc) 54 | .use(format) 55 | .use(html) 56 | .process(vfile.readSync("./test/sample.md"), (error, result) => { 57 | console.error(report(error || result)); 58 | if (error) { 59 | throw error; 60 | } 61 | if (result) { 62 | result.basename = `${filename}.html`; 63 | vfile.writeSync(result); 64 | const ref = vfile.readSync(`./test/${filename}.ref`); 65 | const { same, pretty } = diffVfile(result, ref); 66 | if (same) { 67 | console.log( 68 | `${colors.red("Files do not match")} for ${filename} test.` 69 | ); 70 | console.log(pretty); 71 | exit = 2; 72 | } else { 73 | console.log(`${colors.green("Files match")} for ${filename} test.`); 74 | } 75 | } 76 | }); 77 | }; 78 | 79 | const pluginOptions = { 80 | customTypes: { 81 | custom: { 82 | emoji: "💻", 83 | svg: 84 | '' 85 | } 86 | } 87 | }; 88 | 89 | test(pluginOptions, "svg"); 90 | test({ ...pluginOptions, icons: "emoji" }, "emoji"); 91 | test({ ...pluginOptions, icons: "none" }, "none"); 92 | 93 | process.exit(exit); 94 | -------------------------------------------------------------------------------- /test/svg.ref: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | sample 6 | 7 | 8 | 9 |

This is a test

10 |
11 |
12 |
13 | 14 | Note
15 |
16 |
17 |

This is what a note looks like

18 |
19 |
20 |
21 |
22 |
23 | 24 | Tip
25 |
26 |
27 |

It works great with docusaurus 2.0

28 |
29 |
30 |
31 |
32 |
33 | 34 | you can set your own title
35 |
36 |
37 |

it replaces the default title

38 |
39 |
40 |
41 |
42 |
43 | 44 | credit
45 |
46 |
47 |

Based on remarkable-admonitions

48 |

SVG Icons by GitHub Octicons

49 |
50 |
51 |
52 |
53 |
54 | 55 | Warning
56 |
57 |
58 |

You can't nest them

59 | 67 |
// you can even use block elements
68 | 
69 |
70 |
71 |
72 |
73 |
74 | 75 | Custom
76 |
77 |
78 |

You can make your own custom types. The icon, keyword, and emoji can be set in the plugin options and they can be styled separately.

79 |
80 |
81 | 82 | 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Travis (.com)](https://img.shields.io/travis/com/elviswolcott/remark-admonitions?logo=travis)](https://travis-ci.com/elviswolcott/remark-admonitions) 2 | [![npm](https://img.shields.io/npm/v/remark-admonitions?label=remark-admonitions&logo=npm)](https://www.npmjs.com/package/remark-admonitions) 3 | # remark-admonitions 4 | 5 | > A [remark](https://github.com/remarkjs/remark) plugin for admonitions designed with Docusaurus v2 in mind. 6 | 7 | > `remark-admonitions` is now included out-of-the-box with `@docusaurus/preset-classic`! 8 | 9 | ![example of admonitions](/assets/hero.png) 10 | 11 | # Installation 12 | 13 | `remark-admonitions` is available on NPM. 14 | 15 | ```bash 16 | npm install remark-admonitions 17 | ``` 18 | 19 | ## unified + remark 20 | If you're using unified/remark, just pass the plugin to `use()` 21 | 22 | For example, this will compile `input.md` into `output.html` using `remark`, `rehype`, and `remark-admonitions`. 23 | 24 | ```javascript 25 | const unified = require('unified') 26 | const markdown = require('remark-parse') 27 | // require the plugin 28 | const admonitions = require('remark-admonitions') 29 | const remark2rehype = require('remark-rehype') 30 | const doc = require('rehype-document') 31 | const format = require('rehype-format') 32 | const html = require('rehype-stringify') 33 | const vfile = require('to-vfile') 34 | const report = require('vfile-reporter') 35 | 36 | const options = {} 37 | 38 | unified() 39 | .use(markdown) 40 | // add it to unified 41 | .use(admonitions, options) 42 | .use(remark2rehype) 43 | .use(doc) 44 | .use(format) 45 | .use(html) 46 | .process(vfile.readSync('./input.md'), (error, result) => { 47 | console.error(report(error || result)) 48 | if (result) { 49 | result.basename = "output.html" 50 | vfile.writeSync(result) 51 | } 52 | }) 53 | ``` 54 | 55 | ## Docusaurus v2 56 | 57 | `@docusaurus/preset-classic` includes `remark-admonitions`. 58 | 59 | If you aren't using `@docusaurus/preset-classic`, `remark-admonitions` can still be used through passing a `remark` plugin to MDX. 60 | # Usage 61 | 62 | Admonitions are a block element. 63 | The titles can include inline markdown and the body can include any block markdown except another admonition. 64 | 65 | The general syntax is 66 | 67 | ```markdown 68 | :::keyword optional title 69 | some content 70 | ::: 71 | ``` 72 | 73 | For example, 74 | 75 | ```markdown 76 | :::tip pro tip 77 | `remark-admonitions` is pretty great! 78 | ::: 79 | ``` 80 | 81 | 82 | The default keywords are `important`, `tip`, `note`, `warning`, and `danger`. 83 | Aliases for `info` => `important`, `success` => `tip`, `secondary` => `note` and `danger` => `warning` have been added for Infima compatibility. 84 | 85 | # Options 86 | 87 | The plugin can be configured through the options object. 88 | 89 | ## Defaults 90 | 91 | ```ts 92 | const options = { 93 | customTypes: customTypes, // additional types of admonitions 94 | tag: string, // the tag to be used for creating admonitions (default ":::") 95 | icons: "svg"|"emoji"|"none", // the type of icons to use (default "svg") 96 | infima: boolean, // wether the classes for infima alerts should be added to the markup 97 | } 98 | ``` 99 | 100 | ## Custom Types 101 | 102 | The `customTypes` option can be used to add additional types of admonitions. You can set the svg and emoji icons as well as the keyword. You only have to include the svg/emoji fields if you are using them. 103 | The ifmClass is only necessary if the `infima` setting is `true` and the admonition should use the look of an existing Infima alert class. 104 | 105 | ```ts 106 | const customTypes = { 107 | [string: keyword]: { 108 | ifmClass: string, 109 | keyword: string, 110 | emoji: string, 111 | svg: string, 112 | } | string 113 | } 114 | ``` 115 | 116 | For example, this will allow you to generate admonitions will the `custom` keyword. 117 | 118 | ```js 119 | customTypes: { 120 | custom: { 121 | emoji: '💻', 122 | svg: '' 123 | } 124 | } 125 | ``` 126 | 127 | To create an alias for an existing type, have the value be the keyword the alias should point to. 128 | 129 | ```js 130 | customTypes: { 131 | alias: "custom" 132 | } 133 | ``` 134 | 135 | The generated markup will include the class `admonition-{keyword}` for styling. 136 | 137 | If the `infima` option is `true`, the classes `alert alert--{type}` will be added to inherit the default Infima styling. 138 | 139 | # Styling 140 | 141 | You'll have to add styles for the admonitions. With Docusaurus, these can be added to `custom.css`. 142 | 143 | ## Infima (Docusaurus v2) 144 | 145 | The Infima theme (`styles/infima.css`) is used by `@docusaurus/preset-classic`. 146 | 147 | ![infima theme](assets/infima.png) 148 | 149 | ## Classic (Docusaurus v1) 150 | 151 | The classic theme (`styles/classic.css`) replicates the look of `remarkable-admonitions` and Docusaurus v1. 152 | 153 | ![classic theme](/assets/classic.png) 154 | 155 | # Credit 156 | 157 | Syntax and classic theme based on [`remarkable-admonitions`](https://github.com/favoloso/remarkable-admonitions). 158 | 159 | The SVG icons included are from [GitHub Octicons](https://octicons.github.com). -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // handles different types of whitespace 2 | const unified = require("unified"); 3 | const html = require("rehype-parse"); 4 | const visit = require("unist-util-visit"); 5 | 6 | const NEWLINE = "\n"; 7 | 8 | // natively supported types 9 | const types = { 10 | // aliases for infima color names 11 | secondary: "note", 12 | info: "important", 13 | success: "tip", 14 | danger: "warning", 15 | // base types 16 | note: { 17 | ifmClass: "secondary", 18 | keyword: "note", 19 | emoji: "ℹ️", // 'ℹ' 20 | svg: 21 | '' 22 | }, 23 | tip: { 24 | ifmClass: "success", 25 | keyword: "tip", 26 | emoji: "💡", //'💡' 27 | svg: 28 | '' 29 | }, 30 | warning: { 31 | ifmClass: "danger", 32 | keyword: "warning", 33 | emoji: "🔥", //'🔥' 34 | svg: 35 | '' 36 | }, 37 | important: { 38 | ifmClass: "info", 39 | keyword: "important", 40 | emoji: "❗️", //'❗' 41 | svg: 42 | '' 43 | }, 44 | caution: { 45 | ifmClass: "warning", 46 | keyword: "caution", 47 | emoji: "⚠️", // '⚠️' 48 | svg: 49 | '' 50 | } 51 | }; 52 | 53 | // default options for plugin 54 | const defaultOptions = { 55 | customTypes: [], 56 | useDefaultTypes: true, 57 | infima: true, 58 | tag: ":::", 59 | icons: "svg" 60 | }; 61 | 62 | // override default options 63 | const configure = options => { 64 | const { customTypes, ...baseOptions } = { 65 | ...defaultOptions, 66 | ...options 67 | }; 68 | 69 | return { 70 | ...baseOptions, 71 | types: { ...types, ...customTypes } 72 | }; 73 | }; 74 | 75 | // escape regex special characters 76 | function escapeRegExp(s) { 77 | return s.replace(new RegExp(`[-[\\]{}()*+?.\\\\^$|/]`, "g"), "\\$&"); 78 | } 79 | 80 | // helper: recursively replace nodes 81 | const _nodes = ({ 82 | tagName: hName, 83 | properties: hProperties, 84 | position, 85 | children 86 | }) => { 87 | return { 88 | type: "admonitionHTML", 89 | data: { 90 | hName, 91 | hProperties 92 | }, 93 | position, 94 | children: children.map(_nodes) 95 | }; 96 | }; 97 | 98 | // convert HTML to MDAST (must be a single root element) 99 | const nodes = markup => { 100 | return _nodes( 101 | unified() 102 | .use(html) 103 | .parse(markup).children[0].children[1].children[0] 104 | ); 105 | }; 106 | 107 | // create a simple text node 108 | const text = value => { 109 | return { 110 | type: "text", 111 | value 112 | }; 113 | }; 114 | 115 | // create a node that will compile to HTML 116 | const element = (tagName, classes = [], children = []) => { 117 | return { 118 | type: "admonitionHTML", 119 | data: { 120 | hName: tagName, 121 | hProperties: classes.length 122 | ? { 123 | className: classes 124 | } 125 | : {} 126 | }, 127 | children 128 | }; 129 | }; 130 | 131 | // changes the first character of a keyword to uppercase so that custom title 132 | // styles may omit `text-transform: uppercase`. 133 | const formatKeyword = keyword => 134 | keyword.charAt(0).toUpperCase() + keyword.slice(1); 135 | 136 | // passed to unified.use() 137 | // you have to use a named function for access to `this` :( 138 | module.exports = function attacher(options) { 139 | const config = configure(options); 140 | 141 | // match to determine if the line is an opening tag 142 | const keywords = Object.keys(config.types) 143 | .map(escapeRegExp) 144 | .join("|"); 145 | const tag = escapeRegExp(config.tag); 146 | const regex = new RegExp(`${tag}(${keywords})(?: *(.*))?\n`); 147 | const escapeTag = new RegExp(escapeRegExp(`\\${config.tag}`), "g"); 148 | 149 | // the tokenizer is called on blocks to determine if there is an admonition present and create tags for it 150 | function blockTokenizer(eat, value, silent) { 151 | // stop if no match or match does not start at beginning of line 152 | const match = regex.exec(value); 153 | if (!match || match.index !== 0) return false; 154 | // if silent return the match 155 | if (silent) return true; 156 | 157 | const now = eat.now(); 158 | const [opening, keyword, title] = match; 159 | const food = []; 160 | const content = []; 161 | 162 | // consume lines until a closing tag 163 | let idx = 0; 164 | while ((idx = value.indexOf(NEWLINE)) !== -1) { 165 | // grab this line and eat it 166 | const next = value.indexOf(NEWLINE, idx + 1); 167 | const line = 168 | next !== -1 ? value.slice(idx + 1, next) : value.slice(idx + 1); 169 | food.push(line); 170 | value = value.slice(idx + 1); 171 | // the closing tag is NOT part of the content 172 | if (line.startsWith(config.tag)) break; 173 | content.push(line); 174 | } 175 | 176 | // consume the processed tag and replace escape sequences 177 | const contentString = content.join(NEWLINE).replace(escapeTag, config.tag); 178 | const add = eat(opening + food.join(NEWLINE)); 179 | 180 | // parse the content in block mode 181 | const exit = this.enterBlock(); 182 | const contentNodes = element( 183 | "div", 184 | "admonition-content", 185 | this.tokenizeBlock(contentString, now) 186 | ); 187 | exit(); 188 | // parse the title in inline mode 189 | const titleNodes = this.tokenizeInline( 190 | title || formatKeyword(keyword), 191 | now 192 | ); 193 | // create the nodes for the icon 194 | const entry = config.types[keyword]; 195 | const settings = typeof entry === "string" ? config.types[entry] : entry; 196 | const iconNodes = 197 | config.icons === "svg" ? nodes(settings.svg) : text(settings.emoji); 198 | const iconContainerNodes = 199 | config.icons === "none" 200 | ? [] 201 | : [element("span", "admonition-icon", [iconNodes])]; 202 | 203 | // build the nodes for the full markup 204 | const admonition = element( 205 | "div", 206 | ["admonition", `admonition-${keyword}`].concat( 207 | settings.ifmClass ? ["alert", `alert--${settings.ifmClass}`] : [] 208 | ), 209 | [ 210 | element("div", "admonition-heading", [ 211 | element("h5", "", iconContainerNodes.concat(titleNodes)) 212 | ]), 213 | contentNodes 214 | ] 215 | ); 216 | 217 | return add(admonition); 218 | } 219 | 220 | // add tokenizer to parser after fenced code blocks 221 | const Parser = this.Parser.prototype; 222 | Parser.blockTokenizers.admonition = blockTokenizer; 223 | Parser.blockMethods.splice( 224 | Parser.blockMethods.indexOf("fencedCode") + 1, 225 | 0, 226 | "admonition" 227 | ); 228 | Parser.interruptParagraph.splice( 229 | Parser.interruptParagraph.indexOf("fencedCode") + 1, 230 | 0, 231 | ["admonition"] 232 | ); 233 | Parser.interruptList.splice( 234 | Parser.interruptList.indexOf("fencedCode") + 1, 235 | 0, 236 | ["admonition"] 237 | ); 238 | Parser.interruptBlockquote.splice( 239 | Parser.interruptBlockquote.indexOf("fencedCode") + 1, 240 | 0, 241 | ["admonition"] 242 | ); 243 | 244 | // TODO: add compiler rules for converting back to markdown 245 | 246 | return function transformer(tree) { 247 | // escape everything except admonitionHTML nodes 248 | visit( 249 | tree, 250 | node => { 251 | return node.type !== "admonitionHTML"; 252 | }, 253 | function visitor(node) { 254 | if (node.value) node.value = node.value.replace(escapeTag, config.tag); 255 | return node; 256 | } 257 | ); 258 | }; 259 | }; 260 | --------------------------------------------------------------------------------