Adding Mermaid

Author: Stephen Schleising

Adding Mermaid to this site was a bit of a challenge, so I'm going to explain here how I did it.

Static Pages

It's fairly simple to add Mermaid to a static page (such as this page you are looking at), first of all create a javascript file containing the following,

document.addEventListener("load", event => {
    // Initialise mermaid
    mermaid.mermaidAPI.initialize({startOnLoad:true});
});

Then in your HTML add the following scripts,

<script src="https://cdn.jsdelivr.net/npm/mermaid@9/dist/mermaid.min.js"></script>
<script src="/js/blog/blog.js"></script>

Note

The second script should point to the file containing the onLoad event listener

Once the Mermaid markdown has been converted to HTML it can be added to the page either by copying and pasting it or by adding it through a Jinja2 template.

It is important that all of the HTML is present at the point that initialize() is called on the Mermaid API.

Dynamic Pages

Adding Mermaid to dynamic pages is a bit more difficult. It is no good calling initialize() any more as that function searches for HTML elements with class="mermaid" set. As with a dynamic page these elements are not yet present nothing will happen.

So, what can we do?

Out of the box, not much...

There is a solution, but it's a bit tricky. We can again use the Mermaid API, however, this time we need to call render() ourselves on the individual HTML divs which are mermaid classes.

The prototype for this function is,

mermaid.mermaidAPI.render(elementId, graphDefinition, callback);

Note

  • elementId is the id of the mermaid class div
  • graphDefinition is the markdown that defines the graph
  • callback is a function you provide that will receive the svg for you to insert

By default, the Mermaid renderer I'm using does not supply an id for the divs, therefore we need to generate one ourselves.

This renderer also appears to have been abandoned, so I decided to fork the repository and update it myself.

Server Side

Fix Override of extendMarkdown

The first task was to fix the extension initialisation, the Python markdown package has changed and extendMarkdown no longer requires the md_globals variable

class MermaidExtension(Extension):
    """ Add source code hilighting to markdown codeblocks. """

    def extendMarkdown(self, md, md_globals):
        """ Add HilitePostprocessor to Markdown instance. """
        # Insert a preprocessor before ReferencePreprocessor
        md.preprocessors.register(MermaidPreprocessor(md), 'mermaid', 35)

        md.registerExtension(self)

So we remove that,

class MermaidExtension(Extension):
    """ Add source code hilighting to markdown codeblocks. """

    def extendMarkdown(self, md):
        """ Add HilitePostprocessor to Markdown instance. """
        # Insert a preprocessor before ReferencePreprocessor
        md.preprocessors.register(MermaidPreprocessor(md), 'mermaid', 35)

        md.registerExtension(self)

Next we generate a unique ID for each div produced,

from secrets import choice
...
class MermaidPreprocessor(Preprocessor):
    def run(self, lines):
...
                unique_id = ''.join(choice(string.ascii_letters) for i in range(16))
                new_lines.append(f'<div class="mermaid" id="{unique_id}">')
...

Note

The Python standard library secrets module is used as it ensures the ID is unique (but not necessarily random)

This is all we need to do on the server side, so next we move on to the client side Javascript.

Client Side

Call render() on all mermaid class divs

First of all we find the elements which are of class mermaid, then we loop through them calling render(),

function renderMermaidElements() {
    // Get any divs whose class is mermaid
    mermaidElements = document.getElementsByClassName("mermaid");

    // Loop through the mermaid divs
    for (let index = 0; index < mermaidElements.length; index++) {
        // Get the ID
        id = mermaidElements[index].id;

        // Get the markdown for the image
        innerHTML = htmlDecode(mermaidElements[index].innerHTML);

        try {
            // Render the image, append -svg to the ID so it doesn't trash the existing div
            mermaid.mermaidAPI.render(id + "-svg", innerHTML, mermaidCallback);
        } catch (e) {
            // Ignore the parsing error as this will happen while building up the diagram
        }
    }
};

We have to decode the HTML for the graph definition using the following function,

const htmlDecode = (input) => {
    const doc = new DOMParser().parseFromString(input, "text/html");
    return doc.documentElement.textContent;
};
Handle Callback

The callback function creates an HTML template element, adds the svg to it, then inserts that element into the HTML at the correct place,

function mermaidCallback(svgGraph) {
    // Create a template to hold the svg
    template = document.createElement("template");

    // Add the svg to the template
    template.innerHTML = svgGraph;

    // Get the svg, which is now an element, from the template
    newElement = template.content.firstChild;

    // Get the div element to insert the svg into, the ID is found by stripping the -svg from the svg ID
    mermaidElement = document.getElementById(newElement.id.substring(0, newElement.id.length - 4));

    // Clear the existing children from the mermaid div
    mermaidElement.replaceChildren();

    // Append the svg element as a child of the svg div
    mermaidElement.appendChild(newElement);
}

And that's it, the graphs will now be correctly rendered live as the Markdown is being updated.