├── .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 |
50 | - but
51 | - you
52 | - can
53 | - nest
54 | - other
55 | - markdown
56 |
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 |
50 | - but
51 | - you
52 | - can
53 | - nest
54 | - other
55 | - markdown
56 |
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 |
16 |
17 |
This is what a note looks like
18 |
19 |
20 |
21 |
26 |
27 |
It works great with docusaurus 2.0
28 |
29 |
30 |
31 |
32 |
you can set your own title
35 |
36 |
37 |
it replaces the default title
38 |
39 |
40 |
41 |
42 |
credit
45 |
46 |
47 |
Based on remarkable-admonitions
48 |
SVG Icons by GitHub Octicons
49 |
50 |
51 |
52 |
53 |
Warning
56 |
57 |
58 |
You can't nest them
59 |
60 | - but
61 | - you
62 | - can
63 | - nest
64 | - other
65 | - markdown
66 |
67 |
// you can even use block elements
68 |
69 |
70 |
71 |
72 |
73 |
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 | [](https://travis-ci.com/elviswolcott/remark-admonitions)
2 | [](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 | 
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 | 
148 |
149 | ## Classic (Docusaurus v1)
150 |
151 | The classic theme (`styles/classic.css`) replicates the look of `remarkable-admonitions` and Docusaurus v1.
152 |
153 | 
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 |
--------------------------------------------------------------------------------