Mermaid is a trendy diagramming tool. A year ago, it was integrated into the Markdown rendering of Github. (see https://github.blog/2022-02-14-include-diagrams-markdown-files-mermaid/) It is also integrated into several editors.

What can you do, however, if you use a different editor? What if you want to use your Markdown document in an environment that does not integrate Mermaid yet? What can you do if the diagram is not Mermaid but PlantUML, Graphviz, or any other diagramming tool?

This article will show how you can integrate ANY diagram-as-code tool into your documents. The technique works for Markdown, Asciidoc, APT, or any other text-based markup language.

However, before anything else, here is a demonstration image, which was created the way I will describe in this article.

e8a96c341e35bf3594a44f6a47e198b17c60affc393db847a4e26f7ed05708b5

1. Problem Statement

When documenting some systems, it is often necessary to include diagrams. Keeping diagrams in separate files has advantages but also disadvantages. It is easier to keep the consistency of the documentation when the different parts are close together. The more distanced the two corresponding and related parts are, the more likely that one or the other becomes stale when the other is updated.

It is also a good idea if you can parameterize the diagram, and you could avoid copy-pasting diagram parameters from the document, the documented code, or the other way around.

To solve these problems, more and more markup languages support selected diagramming tool markups to embed in the text. You can include Mermaid in Markdown documents if you target GitHub hosting for your document. You can include PlantUML diagrams in Asciidoc documents.

What happens, however, if you want to include Mermaid in Asciidoc? What if you need PlantUML in Markdown? How do you solve the issue if you want to host your Markdown elsewhere besides GitHub?

You can abandon your ideas, stick to the available tools or wait for a solution. The latter approach, however, will always remain an issue. There will always be a new tool you want to use, and you will have to wait for the support of that tool in your favorite markup language.

The principal reason for this is an architectural mismatch.

Document markup languages must be responsible only for document structure and content and nothing else.

Embedding a diagramming tool into the markup language must not be implemented in these languages. It is a separate concern with the document’s programmability ensuring document consistency automation.

The solution is to use a meta markup above the document markup. This meta markup can be document markup agnostic and support all the diagramming tools you want to use.

2. Ideas and Approach to Solve the problem

The basic idea is not new: separation of concerns. The document markup language should be responsible for the document structure and content. The diagramming tool should be responsible for the diagramming. The meta markup should be responsible for the integration.

Since the meta markup is language agnostic, it can be used with any existing and future document markup languages. There is no need to wait for the support of the diagramming tool in the document markup language. The only question is the integration of the meta markup into the document markup language.

The simplest and loosest way to integrate the meta markup is to use a preprocessor. Processing the meta markup, we read and generate a text file. The document markup processing tool catches where the meta markup has left off. It has no idea that a program generates the document markup and is not manually edited. Strictly speaking, when you edit a document markup, then the editor is the program that generates the file. Technically, there is no difference.

There are other possibilities. Most document markups support different editors to deliver some form of WYSIWYG editing. The meta markup preprocessor can be integrated into these editors. That way, the document markup enriched with the meta markup can seamlessly be edited in the WYSIWYG editor.

The proposed meta markup and the implementing tool, Jamal, follow both approaches.

3. Suggested Solution, Tools

The suggested solution is to use Jamal as the meta markup. Jamal is a general-purpose macro processor. There are other meta markup processing tools, like PyMdown. These tools usually target a specific document markup and a specific purpose.

Jamal is a general-purpose, turning complete macro processor with more than 200 macros for different purposes. These macros make your documents programmable to automate manual document maintenance tasks.

The general saying is: if you could give a task to an assistant to do, then you can automate it with Jamal.

Jamal has a PlantUML module. PlantUML is written in Java, the development language I used to create Jamal. It makes the integration of PlantUML into Jamal easy, and PlantUML diagrams embedded into the documentation can be converted in the process. Jamal, however, is not limited to using only tools written in Java. To demonstrate it, we will use the Mermaid diagramming tool, written in JavaScript and running with node.

Since Mermaid is not a Java program, it cannot be executed inside the JVM. We will create our documentation to execute Mermaid as a separate process. Other diagramming tools can be integrated similarly if executed on the document processing machine.

3.1. Install Mermaid

The first step is to install Mermaid. The steps are documented on the Mermaid site. I will not repeat the steps here because I do not want to create a document that gets outdated.

On my machine, the installation creates the /usr/local/bin/mmdc executable. This file is a JavaScript script that starts the Mermaid diagramming tool. While executing from Jamal, I realized the process has a different environment than my login script. To deal with that, I had to edit the file. Instead of using the env command to find the node interpreter, I specified the full path to the node executable. Other installations may be different, and it does not affect the rest of the article. It is a technical detail.

3.2. Install Jamal

We will use Jamal as the meta markup processor. The installation is detailed in the documentation of Jamal. You can start it from the command line, as a Maven plugin, using Jbang and many other ways. I recommend using it as a preprocessor integrated into the IntelliJ Asciidoctor plugin. It will provide you with WYSIWYG editing of your document in Markdown and Asciidoc enriched with Jamal macros. Also, the installation is nothing more than executing the command line

mvn com.javax0.jamal:jamal-maven-plugin:2.1.0:jamalize

which will download the version 2.1.0 we use in this article by the time pre-release and copy all the needed files into your project’s .asciidoctor/lib directory. It will make the macros available for the Asciidoctor plugin. What needs manual work is configuring IntelliJ to treat all .jam files as Asciidoc files. That way, the editor will invoke the Asciidoctor plugin and use the Jamal preprocessor. It is the setup that I also use to write the articles.

3.3. Create the macros for Mermaid

To have a mermaid document inside the document, we should do three things using macros:

  1. Save the Mermaid text into a file.

  2. Execute the Mermaid tool to convert the text into an SVG file.

  3. Reference the SVG file as an image in the document.

Later, we will see how to save on Mermaid processing, executing it only when the Mermaid text changes.

We will use the io:write macro to save the Mermaid text into a file. This macro is in a package that is not loaded by default. We have to load it explicitly. To do that, we use the maven:load macro.

{@maven:load com.javax0.jamal:jamal-io:2.1.0}
Note

This macro package has to be configured as safe for the document in the .jamal/settings.properties file as it is documented. The macros in this package can read and write files and execute commands configured. To use a macro package like that from an untrusted source is a security risk. For this reason, every package loaded by the maven:load macro has to be configured as safe. The configuration specifies the package and the documents where it can be used.

At the same time, the io package also needs configuration to be able to execute the mmdc command. To do that, the configuration file contains a

mermaid=/usr/local/bin/mmdc

line assigning a symbolic name to the actual command. The io:exec macro will use this symbolic name to find the command to execute.

When the macro package is loaded, we can use the io:write macro as in the following sample:

{#define CHART=flowchart TD
A[Christmas] -->|Get money| B(Go shopping)
B --> C{Let me think}
C -->|One| D[Laptop]
C -->|Two| E[iPhone]
C -->|Three| F[fa:fa-car Care]
}
{#io:write (output="aaa.mmd") {CHART}}

When the file is created, we can execute the Mermaid tool to convert it into an SVG file, as the following

{#io:exec command="""mermaid
-i
aaa.mmd
-o
aaa.svg
""" cwd="." error="convert.err.txt" output="convert.out.txt"
}

By that, we have the file. Whenever the Mermaid text changes, the SVG file will be recreated.

As a matter of fact, whenever the document changes, the SVG file will be recreated. It wastes resources when the diagram remains the same and the processing runs interactively. To help with that, we can use the hashCode macro.

The macro hashCode calculates the hash code of the text. We will use the hash code to name the file. Whenever the diagram changes, the file’s name changes. Also, if the file exists, it should contain the diagram for the current text.

To check that the file exists, we include it in the document. Because we do not want the SVG text in the document, we surround the include with the block macro. If the file does not exist, then an error will occur. The macro try will handle this error, and the execution will continue. However, the macro CREATE will be set to true in this case. If there is no error, when the file already exists, the macro CREATE will be set to false.

The if macro will check the value of the macro CREATE. If it is true, it will execute the io:write and io:exec macros to create the file. If it is false, then it will do nothing.

{#define KEY={#hashCode {CHART}}}{@define CREATE=true}
{@try {#block{#include images/{KEY}.svg}}{@define CREATE=false}}
{#if `//`{CREATE}//
{#io:write (mkdir output="images/aaa.mmd") {CHART}}
{#io:exec command="""mermaid
-i
images/aaa.mmd
-o
images/{KEY}.svg
""" cwd="." error="convert.err.txt" output="convert.out.txt"
}//}

For more details using these macros, please refer to the documentation. If you intend to use multiple diagrams, you may want to create a macro that does all the steps above.

4. Summary and Takeaway

This article discussed integrating Mermaid diagrams into your Asciidoc, Markdown, or any other markup document. We selected Mermaid for two reasons. First, usually, this is the tool people ask for. Second, this is an excellent example of a non-Java tool that can be integrated into document processing. The described way can be applied to any external tool capable of running as a process.

The samples also demonstrate a complex structure of macros showing the power of the Jamal macro processor. Such complexity is rarely needed.

In addition to the technology, I discussed, though only briefly, the separation of concerns for document handling and how the document formatting markup should be separated from the processing meta markup.

If you want to have diagrams in your documentation, download Jamal, and start enhancing your documents.


Comments

Please leave your comments using Disqus, or just press one of the happy faces. If for any reason you do not want to leave a comment here, you can still create a Github ticket.