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 inargs.Html
and it's not a complete document. The Preview template has not been applied yet. You can modifyargs.Html
to affect rendering of the HTML. Additionally you can also setargs.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 inAfterDocumentRendered
because this HTML is always refreshed.
You can change:args.Html
andargs.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-2022 • Updated: 12/22/19
Comment or report problem with topic