Rendering Mermaid graphs in Markdown on GitHub

January 20, 2021
make mermaid markdown github

When writing documentation for software, sooner or later you’re going to hit the point at which a picture will explain in a glance what you would have a hard time describing in the proverbial “thousand words”. While documenting something technical, this usually means diving into the drawing section of whatever editor you’re using and wrangling with lines and boxes until you’re satisfied with your masterpiece or you’ve exhausted your patience with the tool.

I write a fair bit of Markdown which unfortunately doesn’t offer such luxuries as drawing tools and embedded editable image support. Or does it?

Typora

Last year, I started using Typora, an absolutely amazing Markdown editor. Typora lets you write Markdown in its rendered state and the interface is clutter free, reminiscent of the “distraction free” writing apps that were all the rage a few years ago. I was poking around Typora’s docs and saw something interesting; Draw Diagrams With Markdown. As it turns out, Typora will render diagrams embedded in tagged code blocks using one of 3 Javascript graphing libraries, js-sequence, flowchart.js and the most versatile of the three, mermaid.

All three libraries are integrated and invoked using a ``` codeblock–tagged with either sequence, flow or mermaid– that contains the graph definition. I decided to put it to use and drew a pretty big mermaid diagram in a readme file for one of last year’s projects. It was nice to be able to define the graph in code and keep documentation and imagery close together. (imagine, having search-and-replace that covers the labels in your diagrams! 🤯 )

Unfortunately, not all Markdown viewers render mermaid–or any other–graphs, most notably, GitHub does not support them. My solution at the time was to use the Mermaid Live Editor to generate a rendered version of my graph and link that image in the readme file. I also “hid” the graph source in a collapsible <details> block and added a plea to future maintainers, along with an explanation on how to generate the image. Not ideal, but at least it would show the rendered image on GitHub and hide the graph source to readers.

But again, “Not ideal…”.

Automated Mermaid Rendering

I was going to put together a post about Typora and Mermaid and while mulling over what to write, I thought 💡 “what if I automate my work-around for rendering mermaid graphs?”1 A few hours of tinkering later, I came up with a script and a make target that can be included in any GitHub repo2. The script takes a Markdown file and looks for occurrences of blocks that look like this:

![graph image alt-text](path/to/image.png)
<details>
  <summary>diagram source</summary>
  what text goes here, or what is in the summary tags doesn't matter. it gets collapsed along with the following mermaid graph definition
 ```mermaid
graph TD
    A[README.md] -->|passed to| B
    subgraph render-md-mermaid.sh
    B{Find mermaid graphs<br>and image paths} --> C(docker mermaid-cli)
    B --> D(docker mermaid-cli)
    end
    C -->|path/to/image1.png| E[Graph 1 png image]
    D -->|path/to/image2.svg| F[Graph 2 svg image]
```

</details>

Once the script has executed, the graph (and its source) will be rendered as follows:

rendered mermaid graph from the example above

diagram source what text goes here, or what is in the summary tags doesn't matter. it gets collapsed along with the following mermaid graph definition
graph TD
    A[README.md] -->|passed to| B
    subgraph render-md-mermaid.sh
    B{Find mermaid graphs<br>and image paths} --> C(docker mermaid-cli)
    B --> D(docker mermaid-cli)
    end
    C -->|path/to/image1.png| E[Graph 1 png image]
    D -->|path/to/image2.svg| F[Graph 2 svg image]

render-md-mermaid.sh and it’s accompanying make target

#!/usr/bin/env bash
# Usage: render-md-mermaid.sh document.md
#
# This can be invoked on any Markdown file to render embedded mermaid diagrams, provided they are presented in the following format:
#
# ![rendered image description](relative/path/to/rendered_image.svg)
# <details>
#   <summary>diagram source</summary>
#   This details block is collapsed by default when viewed in GitHub. This hides the mermaid graph definition, while the rendered image
#   linked above is shown. The details tag has to follow the image tag. (newlines allowed)
#
# ```mermaid
# graph LR
#     A[README.md] -->|passed to| B
#     subgraph render-md-mermaid.sh
#     B{Find mermaid graphs<br>and image paths} --> C(docker mermaid-cli)
#     B --> D(docker mermaid-cli)
#     end
#     C -->|path/to/image1.png| E[Graph 1 png image]
#     D -->|path/to/image2.svg| F[Graph 2 svg image]
# ```
# </details>
#
# The script will pick up the graph definition from the mermaid code bloc and render it to the image file and path specified in the
# image tag using the docker version of mermaid-cli. The rendered image can be in svg or png format, whatever is specified will be generated.


markdown_input=$1
image_re=".*\.(svg|png)$"
echo "Markdown file: $markdown_input"
if [ "$1" == "" ]; then
  echo "Usage: $0 document.md"
  echo "$(tput setaf 1)No Markdown document specified$(tput sgr0)"
  exit 1
fi
rm .render-md-mermaid-config.json .render-md-mermaid.css
mermaid_config='{"flowchart": {"useMaxWidth": false }}'
mermaid_css='#container > svg { max-width: 100% !important; }'
echo "$mermaid_config" >> .render-md-mermaid-config.json
echo "$mermaid_css" >> .render-md-mermaid.css

mermaid_file=""
IFS=$'\n'
for line in $(perl -0777 -ne 'while(m/!\[.*?\]\(([^\)]+)\)\n+<details>([\s\S]*?)```mermaid\n([\s\S]*?)\n```/g){print "$1\n$3\n";} ' "$markdown_input")
do
    if [[ $line =~ $image_re ]]; then
        mermaid_file="$line.mermaid"
        if [[ ! "$mermaid_file" =~ ^.*/.* ]]; then
            mermaid_file="./$mermaid_file"
        fi
        mkdir -p -- "${mermaid_file%/*}"
    else
        echo "$line" >> "$mermaid_file"
    fi
done;
for mermaid_img in $(find . -name "*.mermaid" | sed -E 's/((.*).mermaid)/\2|\1/')
do
    image_file=${mermaid_img%|*}
    mermaid_file=${mermaid_img#*|}
    docker run --rm -t -v "$PWD:/data" minlag/mermaid-cli:latest -o "/data/$image_file" -i "/data/$mermaid_file" -t neutral -C "/data/.render-md-mermaid.css" -c "/data/.render-md-mermaid-config.json" -s 4
    if [[ "$image_file" =~ ^.*\.svg$ ]]; then
        sed -i ".bak" -e 's/<br>/<br\/>/g' $image_file
    fi
    rm "$mermaid_file" "$image_file.bak"
done
rm .render-md-mermaid-config.json .render-md-mermaid.css

There’s quite a bit going on there, but the main bits are:

The script can be invoked manually, but I’ve found it handy to invoke it in a make workflow. That way, graphs are rendered without having to give it a single thought.

render-md-mermaid: $(shell find . -name "render-md-mermaid.sh") $(shell find . -name "*.md") ## Render all mermaid graphs in any .md file in the repository
	@for md in $(shell find . -name "*.md"); do "$<" "$$md"; done

The make target uses a little trick to find the script by referencing it as a prerequisite along with all .md files in the repository. It then loops through the .md files and invokes render-md-mermaid.sh. ($< is set to the first prerequisite)

Conclusion

I’m planning to put this script and other repository utilities in a separate repo on GitHub. This repo can be included as a submodule and provide its utilities to any project, without introducing a bunch of duplicated code. More on that later.

Update: I ended up writing a GitHub Action that uses this script with minimal setup required.

For now, I hope this is useful to someone. If so, or if you have suggestions, shoot me a note!

-@niels


  1. turns out, I was not the first to automatically render mermaid ↩︎

  2. at some point I will formalize this and other “repo” utilities in a separate project Done! ↩︎

Automating npm install

May 18, 2021
I’ve previously written about using Make as a command interface for your repository and the other day, I ran into a situation where using Makefiles takes away a real-life annoyance for teams working on NodeJS and other Javascript or TypeScript based projects. When growing teams work in repositories that use NPM for Yarn for package management, dependency updates will enter the local workspace frequently when the main branch is pulled. Unless you’re religious about running npm install after every git pull, updating local dependencies of often omitted and sometimes leads to confusing red herring bugs or in the very least, lost time...

devops make npm

render-md-mermaid, a GitHub Action

January 31, 2021
Earlier, I wrote about rendering Mermaid graphs in Markdown on GitHub by invoking a script, possibly using a make target to cover all Markdown files in an entire repository. Since then I’ve used Mermaid diagrams and the script presented in that post in README.md docs for a couple of projects. While the script works great, having to remember to run it or plumb client side automation to do so automatically can be a bit of a hassle...

mermaid markdown github

Bye, Wordpress!

January 9, 2021
Last summer, frustrated over the loss of my personal domain, I bought a cute url and set up a Wordpress blog with the intention of sharing some content about what I was working on. Our son was born around the same time and so the site sat idle for a few months, but when I picked up writing back in November, the experience with Wordpress irked me. Wordpress is a fine CMS, it is great for getting a quick blog up and going, but I wanted more control over my content...

blog github hugo