Using RenderExtensions to customize Html output

If you're creating a Markdown Monster addin that wants to customize the HTML rendering process for the HTML that is created you can do this by creating a RenderExtension implementing an IRenderExtension interface.

This is a two step process:

  • Create your IRenderExtension derived class implementation
  • Add your extension to the Extension Manager

The IRenderExtension Interface

RenderExtensions work by providing pre- and post-rendering hook methods that allow you inspect and modify the incoming markdown and outgoing HTML content.

The IRenderExtension interface is defined as follows (github):

/// <summary>
/// Interface implemented for RenderExtensions that allow modification
/// of the inbound Markdown before rendering or outbound HTML after
/// rendering as well as any custom code that needs to be injected
/// into the document header prior to rendering.
///
/// Use the `RenderExtensionsManager.Current.RenderExtensions.Add()` to
/// add any custom extensions you create.
/// </summary>
public interface IRenderExtension
{
    void BeforeMarkdownRendered(ModifyMarkdownArguments args);
    void AfterMarkdownRendered(ModifyHtmlAndHeadersArguments args);
    void AfterDocumentRendered(ModifyHtmlArguments args);
}

Interface Members

The interface provides 3 different hooks:

  • BeforeMarkdownRendered
    Fired just before markdown is rendered into HTML. You can look at and modify the Markdown text and thus affect the render process. You also get passed the document for additional information and you have of course access to the model.
    You can change: args.Markdown

  • AfterMarkdownRendered
    Occurs just after the Markdown text has been rendered into an HTML Fragment. The result HTML is in args.Html and it's not a complete document. The Preview template has not been applied yet. You can modify args.Html to affect rendering of the HTML. Additionally you can also set args.HeadersToEmbed to apply headers that are rendered into the <head> section of the template when the template is rendered later in the pipeline. If you can modify HTML from the rendered Markdown, it's better to do it here, than in AfterDocumentRendered because this HTML is always refreshed.
    You can change: args.Html and args.HeadersToEmbed

  • AfterDocumentRendered
    Occurs after the final HTML document has been rendered and merged with the Preview template. This is essentially the final HTML output before output is written to disk, returned as a string or used for previewing the HTML.
    You can change: args.Html

The args parameter passed contains the input and output data and the updateability of the properties for each of those parameter values depends on the property's readwrite or readonly status.

Hooking up a RenderExtension in OnApplicationStart()

RenderExtensions are global and you can add to the collection at any point. For example:

RenderExtensionsManager.Current.RenderExtensions.Add(new KavaDocsRenderExtension());

The best place to do this in an Addin is in in OnApplicationStart() early on. The extensions aren't loaded until a preview is generated, but you'll want to install it before the first render just to ensure that the initial render during startup sees all the customization. Events that fire after OnApplicationStart() may not fire until after the first document has been rendered as addins load asynchronously in the background.

Simplest Example: Implementing a RenderExtension

You can implement a RenderExtension in a Markdown Monster Addin. At its simplest you can implement a RenderExtension that modifies the content of the output like this. The example uses all the methods but obviously in your own work you may only need to override behavior in one of those methods.

public class KavaDocsRenderExtension : IRenderExtension
{
    // Inject markdown text into the bottom of the document
    public void BeforeMarkdownRendered(ModifyMarkdownArguments args)
    {
        args.Markdown += $"\n\n<small>generated by KavaDocs {DateTime.Now.ToString("d")}</small>\n\n";
    }

    // Add Headers for the `<head>` section of the template
    // and optionally modify the rendered Markdown HTML
    public void AfterMarkdownRendered(ModifyHtmlAndHeadersArguments args)
    {
        // args.HeadersToEmbed = "<script src=''></script>";
        // args.Html = args.Html.Replace("<script>","<bogus>");
        args.HeadersToEmbed = "<script src='https://cdn.jsdelivr.net/npm/vue'></script>";

        // add into the HTML content
        // Note if you add script make sure it 'reloads' itself as the page
        // may not completely re-render, only a small script block does
        args.Html += @"
            <script>
            $(document).on('previewUpdated',function() {
                // do something with vueJs
                var v = new Vue('#MainContent');
            
                // silly stuff...
                setTimeout(function() { $('pre>code').css('background','darkgreen');  },3000);
            });
            </script>";
    }

    // update the final HTML that involves HTML that is part of the template
    public void AfterDocumentRendered(ModifyHtmlArguments args)
    {
        args.Html = args.Html.Replace("</body>", "\n\n<h2>Kava Docs rendered extension</h2>\n\n</body>");
    }
}

This is frivolous, but it demonstrates how you can easily modify the content of the generated output in three ways:

  • Modifying the Markdown text prior to rendering
  • Adding content to the <head> of the document
  • Modifying the final generated HTML output

The result of the above is:

  • A small footer just below the content generated by KavaDocs text
  • A large footer at the very bottom of the document rendered extension text
  • A script tag in the header that loads VueJs
  • Script blocks turn green after a couple of seconds from the injected <script> code
  • On re-rendering script blocks show normal then turn green after 2 seconds
Refreshing Page Content in Script Code

If you inject script code into the page, realize that the script may have to be refired as content is refreshed. Markdown Monster doesn't re-render the entire page all the time, but rather replaces the content area with the rendered HTML content.

To allow script code to respond properly to this, there's an previewUpdated event that is fired when the document first loads and also when the preview is refreshed. So whatever initialization your script code needs, it's best to do this by handling this event.

$(document).on('previewUpdated',function() {
    // do something with vueJs
    var v = new Vue('#MainContent');

    // silly stuff...
    setTimeout(function() { $('pre>code').css('background','darkgreen');  },3000);
});

A more practical example

Here's a more practical example that's provided as part of Markdown Monster which is the Mermaid charting addin. Mermaid is a JavaScript library that uses specific syntax embedded in HTML to render a host of relationship charts.

In order to render these MM needs to:

  • Inject the Mermaid script from CDN
  • Inject Mermaid initialization code
  • Convert ```mermaid into <div class='mermaid'>

Here's the code:

/// <summary>
/// Handles Mermaid charts based on one of two sytnax:
///
/// * Converts ```mermaid syntax into div syntax
/// * Adds the mermaid script from CDN
/// </summary>
public class MermaidRenderExtension : IRenderExtension
{
    /// <summary>
    /// Add script block into the document
    /// </summary>
    /// <param name="args"></param>
    public void AfterMarkdownRendered(ModifyHtmlAndHeadersArguments args)
    {
        if (args.Markdown.Contains(" class=\"mermaid\"") || args.Markdown.Contains("\n```mermaid"))
            args.HeadersToEmbed = MermaidHeaderScript;
    }
    
    /// <summary>
    /// Check for ```markdown blocks and replace them with div blocks
    /// </summary>
    /// <param name="args"></param>
    public void BeforeMarkdownRendered(ModifyMarkdownArguments args)
    {
        while (true)
        {
            string extract = StringUtils.ExtractString(args.Markdown, "\n```mermaid", "```", returndelimiters: true);
            if (string.IsNullOrEmpty(extract))
                break;

            string newExtract = extract.Replace("```mermaid", "<div class=\"mermaid\">")
                .Replace("```", "</div>");

            args.Markdown = args.Markdown.Replace(extract, newExtract);
        }
    }

    public void AfterDocumentRendered(ModifyHtmlArguments args)
    {     }



    private const string MermaidHeaderScript =
        @"<script src=""https://cdnjs.cloudflare.com/ajax/libs/mermaid/7.1.2/mermaid.min.js""></script>
<script>
mermaid.initialize({startOnLoad:false});
...
</script>";

}

If you need to build custom output generation functionality for Markdown Monster for creating custom syntax or simply intercepting HTML rendering output, RenderExtensions are an easy way to do this.


© West Wind Technologies, 2016-2023 • Updated: 12/22/19
Comment or report problem with topic