So, earlier, in Make Your Mark(Down) – Part 1 I tried to introduce you to an editable Markdown control with preview, the next thing we decided we wanted was charts/graphs (courtesy of Mermaid) and some reasonable syntax highlighting which we achieved using highlight.js.

So, we added the relevant packages using Yarn (as is the way in our organisation)

yarn add mermaid highlight.js

Then we needed a whole new way for us to handle rendering – this is where the renderer for marked came in handy. Basically, we created a custom renderer for code and changed some of the rules in order to ensure that we ended up with the expected behaviour.

First off, we needed to create a new renderer, and tell it how to output for the Mermaid controls and charts:

private async createRenderer() {
        // Create the renderer instance
        const renderer = new marked.Renderer();
        renderer.code = (code, language) => {
                // We use the definitions given to us from Mermaid's control
                if (code.match(/^(graph|sequenceDiagram)/)) {
                    // Create a mermaid div
                    return '<div class="mermaid">' + code + '</div>';
                }
        }
        // return this renderer as constructed above
        return renderer;
    }

Now, whenever we render the markdown (using the marked function from part 1), we also pass in this renderer

const renderer = await createRenderer();
const result = await marked(input, {renderer, async: true});

However, this doesn’t seem to do anything about the code formatting – so for that we had to modify the code as follows

private async createRenderer() {
        // Create the renderer instance
        const renderer = new marked.Renderer();
        renderer.code = (code, language) => {
            try {
                // We use the definitions given to us from Mermaid's control
                if (code.match(/^(graph|sequenceDiagram)/)) {
                    // Create a Mermaid div
                    return '<div class="mermaid">' + code + '</div>';
                }
                // Check if HighlightJS has a definition for the language
                if (hljs.getLanguage(language)) {
                    // If it does, render it as defined
                    return '<pre><code>' + hljs.highlight(code, { language }).value + '</code></pre>';
                }
                // If not, attempt automatic detection and highlight accordingly
                return '<pre><code>' + hljs.highlightAuto(code).value + '</code></pre>';
            } catch (e) {
                // And if something goes wrong, don't bother highlighting
                return '<pre><code>' + code + '</code></pre>';
            }
        }
        // Return the renderer as constructed above
        return renderer;
    }

And that’s sort of it – the full snippet is as below

import { Renderer, marked } from "marked";
import mermaid from "mermaid";
import hljs from 'highlight.js';
import 'highlight.js/styles/github.css';

class MarkdownRenderer {
    private renderer: Renderer;

    static create(input: HTMLInputElement, target: HTMLElement) {
        return new MarkdownRenderer(input, target);
    }

    private async createRenderer() {
        const renderer = new marked.Renderer();
        renderer.code = (code, language) => {
            try {
                if (code.match(/^(graph|sequenceDiagram)/)) {
                    return '<div class="mermaid">' + code + '</div>';
                }
                if (hljs.getLanguage(language)) {
                    return '<pre><code>' + hljs.highlight(code, { language }).value + '</code></pre>';
                }
                return '<pre><code>' + hljs.highlightAuto(code).value + '</code></pre>';
            } catch (e) {
                return '<pre><code>' + code + '</code></pre>';
            }
        }
        return renderer;
    }

    private constructor(element: HTMLInputElement, private target: HTMLElement) {
        this.createRenderer()
            .then((renderer) => {
                this.renderer = renderer;
            }).then(() => {
                element.addEventListener('keyup', async (event) => {
                    await this.renderMarkdown((event.target as HTMLInputElement).value);
                    await mermaid.run();
                });
            });
    }

    private async getMarkdown(markdown: string) {
        return await marked(markdown, { renderer: this.renderer, async: true })
    }

    private async renderMarkdown(markdown: string) {
        this.target.innerHTML = await this.getMarkdown(markdown);
    }
}

As always, this code is subject to our normal terms and if you want more info, just let me know!