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:
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 definitiongraph 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:
- a beefy regular expression that matches multiple lines, from the markdown image tag, followed by a
details
block opening to a markdown code block ending before the end of thatdetails
block - this regular expression is executed using perl, which was the only way I found to reliably run a multi-line matching regex
- we loop over each line captured by the regex and filter out the image filename and the graph source. the graph source is written to a temporary file
- next, for every graph found, it invokes
mermaid-cli
usingdocker run
- finally, for svg output we work around a bug where rendered newlines in the svg code are not properly formatted
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!
-
turns out, I was not the first to automatically render mermaid ↩︎
-
at some point I will formalize this and other “repo” utilities in a separate projectDone! ↩︎