Skip to content

Commit

Permalink
Initial commit for remark-mermaid-dataurl plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
aloisklink committed Jun 15, 2020
1 parent 022215c commit 6dd7107
Show file tree
Hide file tree
Showing 10 changed files with 6,812 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
extends: ["plugin:prettier/recommended", "plugin:node/recommended"],
};
24 changes: 24 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Publish NPM Package

on:
release:
types: [published]

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# with:
# submodules: true
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: "12.x"
registry-url: "https://registry.npmjs.org"
- name: Install NPM Packages
run: npm ci
- name: Publish Package
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
37 changes: 37 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Test

on:
push: {}
pull_request: {}

jobs:
deploy:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
steps:
- uses: actions/checkout@v2
# with:
# submodules: true
- name: Cache node modules
uses: actions/cache@v1
env:
cache-name: cache-node-modules
with:
path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS
key: ${{ runner.os }}-${{ env.cache-name }}-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-${{ env.cache-name }}-${{ matrix.node-version }}-
${{ runner.os }}-${{ env.cache-name }}-
${{ runner.os }}-
- name: Setup Node ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install NPM Packages
run: npm ci
- name: Lint
run: npm run lint
- name: Build
run: npm test
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,34 @@
# remark-mermaid-dataurl

A remark plugin for Markdown that replaces mermaid graphs with dataurls

Designed for use with Docusaurus v2.

## Usage with Docusaurus

_see https://v2.docusaurus.io/docs/markdown-features#configuring-plugins for more info_

First, install this plugin:

```bash
npm install --save remark-mermaid-dataurl
```

Then, add them to your `@docusaurus/preset-classic` options in `docusaurus.config.js`:

```js
module.exports = {
// ...
presets: [
[
"@docusaurus/preset-classic",
{
docs: {
// ...
remarkPlugins: [require("remark-mermaid-dataurl")],
},
},
],
],
};
```
156 changes: 156 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
const fs = require("fs");
const childProcess = require("child_process");
const os = require("os");
const path = require("path");

const visit = require("unist-util-visit");
const mmdc = require.resolve("@mermaid-js/mermaid-cli/index.bundle.js");

const PLUGIN_NAME = "remark-mermaid-dataurl";

/**
* Deletes a folder, (essentially does `rmdir(tmpdir, {recursive: true})`)
*
* We can't use rmdir(p, {recursive: true}) since it isn't fully supported
* in Node.Js yet.
* @param {string} tmpdir The directory to delete
*/
async function cleanup(tmpdir) {
const files = await fs.promises.readdir(tmpdir);
await Promise.all(
files.map((file) => {
return fs.promises.unlink(path.join(tmpdir, file));
})
);
await fs.promises.rmdir(tmpdir);
}

/**
* Calls mmdc (mermaid-cli) with the given keyword args
* @param {{[key: string]: any}} kwargs
* Args passed to mmdc in format `--key value`
* @returns {Promise<void, Error>}
*/
async function renderMermaidFile(kwargs) {
const argPairs = Object.keys(kwargs).map((key) => [`--${key}`, kwargs[key]]);
const args = [].concat(...argPairs); // flatten
const process = childProcess.fork(mmdc, args, { silent: true });

return new Promise((resolve, reject) => {
let exited = false; // stream may error AND exit
process.on("error", (error) => {
exited = true;
reject(error);
});
process.on("exit", (code, signal) => {
if (exited) {
return; // already resolved Promise
}
if (code) {
reject(
new Error(
`${mmdc} with kwargs ${JSON.stringify(
kwargs
)} failed with error code: ${code}`
)
);
}
if (signal) {
reject(
new Error(
`${mmdc} with kwargs ${JSON.stringify(
kwargs
)} recieved signal ${signal}`
)
);
}
resolve();
});
});
}

/**
* Creates an SVG from the given mermaid input text.
* @param {string} inputText The mermaid text to render.
* @param {Object} mermaidCliOptions Options to pass to mermaid-cli
* @returns {Promise<string>} The contents of the rendered SVG file.
*/
async function renderMermaidText(inputText, mermaidCliOptions) {
const tmpdir = await fs.promises.mkdtemp(
path.join(os.tmpdir(), "remark-mermaid-")
);
const inputPath = path.join(tmpdir, "input");
const outputPath = path.join(tmpdir, "output.svg");
try {
await fs.promises.writeFile(inputPath, inputText, { encoding: "utf8" });
await renderMermaidFile({
...mermaidCliOptions,
input: inputPath,
output: outputPath,
});
return await fs.promises.readFile(outputPath, { encoding: "utf8" });
} finally {
await cleanup(tmpdir);
}
}

/** Converts a string to a base64 string */
function btoa(string) {
return Buffer.from(string).toString("base64");
}

function dataUrl(data, mimeType, base64 = false) {
if (base64) {
return `data:${mimeType};base64,${btoa(data)}`;
} else {
return `data:${mimeType},${encodeURIComponent(data)}`;
}
}

async function transformMermaidNode(node, file, index, parent, { mermaidCli }) {
const { lang, value, position } = node;
try {
const data = await renderMermaidText(value, mermaidCli);
const newNode = {
type: "image",
title: "Diagram generated via mermaid",
url: dataUrl(data, "image/svg+xml;charset=UTF-8"),
};

file.info(`${lang} code block replaced with graph`, position, PLUGIN_NAME);
// replace old node with current node
parent.children[index] = newNode;
} catch (error) {
file.fail(error, position, PLUGIN_NAME);
}
}

/**
* Remark plugin that converts mermaid codeblocks into self-contained SVG dataurls.
* @param {Object} options
* @param {Obejct} options.mermaidCli Options to pass to mermaid-cli
*/
function remarkMermaid({ mermaidCli = {} } = {}) {
const options = { mermaidCli };
/**
* Look for all code nodes that have the language mermaid,
* and replace them with images with data urls.
*
* @param {Node} tree The Markdown Tree
* @param {VFile} file The virtual file.
* @returns {Promise<void>}
*/
return async function (tree, file) {
const promises = []; // keep track of promises since visit isn't async
visit(tree, "code", (node, index, parent) => {
// If this codeblock is not mermaid, bail.
if (node.lang !== "mermaid") {
return node;
}
promises.push(transformMermaidNode(node, file, index, parent, options));
});
await Promise.all(promises);
};
}

module.exports = remarkMermaid;
Loading

0 comments on commit 6dd7107

Please sign in to comment.