Enhancing Elixir Documentation with Mermaid Charts
Elixir makes it crazy easy to document your code. Documentation really is a first class citizen in Elixir-land: just add your @moduledoc
or @doc
blocks and you’re on your way. You can even test the examples in your documentation to verify that they are accurate: no bad examples allowed! Add the ex_doc
package and then running mix docs
will generate beautifully formatted HTML pages (or an epub eBook) all about your app. I have not once felt the need to scratch out some add-on readthedocs.org-esque solution when the native documentation was already so presentable.
Except for charts. Because OTP applications are so often architected around passing messages, it is really important to have good visibility on the components involved and how they connect to one another. On more than one occasion, this resulted in me laboriously crafting bad Draw.io sketches that inevitably got shoved into some forgotten Confluence page where users would rarely see them. Those charts quietly rotted away like so much documentation when it is kept too far away from the code.
Then I discovered Mermaid Charts. Mermaid uses a simple markdown-like syntax to create some beautiful graphs. My interest was piqued. Can you generate charts dynamically from within Elixir documentation?
Yes you can!
How to add Mermaid Charts to an Elixir App
A prerequisite is that you install the ex_doc
package. Add it to your mix.exs
but there’s no need to include it in the runtime, so take advantage of the various dependency options:
{:ex_doc, "~> 0.24.2", only: [:dev], runtime: false}
Next, edit your mix.exs
so the docs
keyword list defines the before_closing_body_tag
option to where you can add in the Mermaid javascript and set its configuration options, e.g.
def project do
[
# ...
docs: [
before_closing_body_tag: fn
:html ->
"""
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
<script>mermaid.initialize({startOnLoad: true})</script>
"""
_ -> ""
end
]
]
(I had previously leveraged the javascript_config_path
for that purpose, but don’t touch that option: that powers the version dropdown on your final documentation page.) If you want to use Font Awesome Icons, you can simply add the <script>
tag to that same block (use the URL provided by your Font Awesome account):
<script src="https://kit.fontawesome.com/xxxxxxx.js" crossorigin="anonymous"></script>
What we’re going for is adding the Mermaid markdown-ish chart definition right inside the @moduledoc
(placed nicely inside a div
). It ends up looking something like this:
@moduledoc """
My documentation goes here...<div class="mermaid">
graph TD;
classDef server fill:#D0B441,stroke:#AD9121,stroke-width:1px;
classDef topic fill:#B5ADDF,stroke:#312378,stroke-width:1px;
classDef db fill:#9E74BE,stroke:#4E1C74,stroke-width:1px;T1(TopicA):::topic --> G1{{GenServerA}}:::server;
T1(TopicA):::topic --> G2{{GenServerB}}:::server;
G2{{GenServerB}}:::server --> T2(TopicB):::topic;
T2(TopicB):::topic ==> DB[("Storage#nbsp;")]:::db;
</div>
"""
But remember that you want to generate the docs dynamically, and that’s where you can take advantage of some nifty tricks. You can interpolate variables inside your documentation blocks, e.g.
@version "v1.2.3"
@moduledoc """
This is version #{@version} of the software.
"""
But instead of a simple string, you’ll want to include a list of the things that should be included in your chart, e.g.
@things [%{type: :server, name: :alpha}, ...]@moduledoc """
Here's the chart:
<div class="mermaid">
#{SomeHelperModule.make_chart(@things)}
</div>"""
It’s helpful to put your chart-drawing code in a dedicated helper module: you can’t make calls to functions in the same module (i.e. the one you’re trying to document) when that module is being compiled, but you can call out to some other module. You can use EEx to parse your own templates for the purpose. However you do it, you just need to piece together a string that represents the chart.
Make sure your configuration is static — @moduledoc
(and any other module attributes) are evaluated at compile time. So you don’t want it full of calls to other functions. You may need to shuffle things around in your application.ex
start (or whatever you are charting) so it can consume a static list of definitions.
Tips
Don’t forget semi-colons ;
at the end of the lines! A lot of the Mermaid examples omit them because they are optional when line breaks are present, but ex_doc
strips out newlines so the semi-colons are required! After; every; line; otherwise the chart generation will fail.
Go slowly. Make a change and re-run mix docs
to view the change. Charts can break easily and you don’t have any debugging info to work with when Mermaid encounters invalid syntax. It’s also helpful to work out a draft of your chart in a static HTML file so you can view the changes more quickly without having to re-generate the docs.
Remember: use three (!!!) colons: :::
to designate a class (if you are using that particular Mermaid feature). You might inadvertently type two colons because you’re so used to Elixir type specs.
If you are using font-awesome icons, you may find that a lot of icons don’t seem to work (?), and you may need to pad your descriptions with a non-breaking space, otherwise the words tend to get cut off. e.g.
Database[("fa:fa-database Storage#nbsp;")]
If your chart includes a lot of data, you’ll find that the default styling squashes it so small that it becomes unreadable. You can do some CSS hacks for the chart size like the following:
<style>
.mermaid {width:300% !important}
</style>
You can also play around with the chart orientation, e.g. LR;
for left-to-right vs. TD;
for top-down.
Here is an example app where components are graphed dynamically using Mermaid charts: https://github.com/fireproofsocks/mermaid-demo
Happy charting!