Table of Contents
Introduction
I'm a recent convert to markdown. I love it. It's faster to write than HTML; has exquisite brevity, there's no need for angle brackets, and spell checking is easier to achieve. You get the full power of HTML—you can intermingle HTML in markdown if you need it—but you're left with a document that is much easier to edit, navigate, and so forth.
While the CodeProject's article editor (aka the Submission Wizard) is a great way to submit an article, in the past I've preferred to write my article in a text editor, such as word, and then paste it into the CodeProject editor. After which, I usually spend some time formatting code blocks and fixing this and that, which orphans my original document; once you start editing in the CodeProject editor, there's no going back.
For that reason I've been hoping for markdown support in CodeProject. It's not here yet, and I don't know if it ever will be. But, no matter, I've come up with an alternative way to write your CodeProject article in markdown and copy and paste the generated HTML into the CodeProject article editor.
I've created a markdown parser for Markdown Monster. Markdown Monster is an open-source WPF application created by Rick Strahl. It's a paid product, but the free version merely produces a created with Markdown Monster footer in the generated HTML. One of the features of Markdown Monster is that Rick has created a very nice extensibility system for it. There's even a Visual Studio extension that allows you to quickly create a Markdown Monster add-in, and because Markdown Monster is a WPF app, its a piece of cake to debug your add-ins.
A couple of days ago, while preparing to publish my CodeProject article Invisible Ink, which I wrote in markdown, I thought to myself, do I really want to go and reformat all my code blocks to be compatible with CodeProject? Why not spend that time creating a custom markdown parser, that spits out pre
tags compatible with CodeProject's syntax? So, that's what I did.
Since I began using Markdown Monster a few weeks ago, I've created 3 add-ins (including this one) for improving my own productivity. There's a Table of Contents Generator add-in and a Listing and Figure Auto-Number add-in, and, as you see in this article, the CodeProject Markdown Parser. All three add-ins are available in the Markdown Monster Add-In Manager. (See Figure 1.)
I'm writing this article in Markdown Monster. Using my add-ins I was able to generate a table of contents with a click of a button and auto-number all the figures and listings.
In this article, you see how to use the add-in. You then explore the add-in's implementation. You look at creating a custom Markdig code block renderer to correctly generate CodeProject pre
tags from markdown fenced code blocks. You see how to create a custom theme for the preview viewer in Markdown Monster and, finally, at how to install the theme to Markdown Monster's theme folder.
Solving the Compatibility Issue
To enable syntax highlighting in a CodeProject article, you either use the format drop-down box in the article editor, or you manually decorate your pre
tags with a lang
attribute, for example:
<pre lang="cs">
...
</pre>
The trouble is, this syntax isn't produced by any markdown parser I know of. Until now of course.
The syntax produced by Markdig, which is the default markdown parser in Markdown Monster, produces HTML that resembles the following:
<pre><code class="language-csharp">
...
</code></pre>
If you paste HTML that looks like that into the CodeProject article editor, it won't know what to do with it; no syntax highlighting, just ugly strangely formatted text.
Using the add-in, outlined in this article, you can ouput HTML that conforms to the CodeProject format.
Using the Add-In
If you haven't already, install Markdown Monster. Once installed, open the Add-In Manager from Markdown Monster's Tools menu. Click the install button next to the CodeProject Markdown Parser item. Restart Markdown Monster to repopulate the markdown parser and theme drop-down boxes.
Select CodeProject Markdown Parser from the markdown parser drop-down box. (See Figure 2.)
With the CodeProject Markdown Parser selected, the generated HTML produces pre
tags that conform to the CodeProject convention.
NOTE: While writing an article, you may prefer to stick with Markdig as your markdown parser. You only need to switch to the CodeProject Markdown Parser when you are pasting the article into the CodeProject article editor. CodeProject formats code blocks on the fly, so with the CodeProject Markdown Parser selected, you won't see any syntax highlighting in your code blocks until you preview the article on CodeProject.
If you don't care about seeing a preview of the syntax highlighting, and would rather preview your article using CodeProject CSS, then select the CodeProject theme from the theme drop down list. (See Figure 3.)
NOTE: You do not need to use the CodeProject theme for your article to be rendered properly for CodeProject.
TIP: When writing an article in Markdown Monster, place all images, downloads, and so forth in the same directory as your articles markdown file. That way, no changes are needed when you come to publish your article on CodeProject.
Exporting Your Finished Article
Once you've finished writing your article, and you wish to view it on CodeProject, tap the Copy HTML to Clipboard button. (See Figure 4.)
An 'HTML copied to clipboard' message is displayed in Markdown Monster's status bar.
Switch to your CodeProject article in the web browser and select 'Source' from the editor toolbar. (See Figure 5.)
Press Ctrl-A Ctrl-V to remove the current content from the editor and paste the text from Markdown Monster.
Voila, you've got your article, which was written in markdown, successfully rendering on CodeProject *hopefully*. Click the Preview button to make sure its rendering correctly.
Exploring the Inner Workings of the Add-In
In this section you look at how the add-in substitutes CodeProject compatible pre
tags, and how the Copy to Clipboard toolbar button works. Finally, you see how to create and deploy a custom Markdown Monster theme.
I'm not going to cover the ins and outs of creating a Markdown Monster add-in, because Rick has already done that. The Markdown Monster help contains step by step instructions from installing the Visual Studio extension, writing the add-in code, to packaging and publishing the add-in.
Supplanting the Markdig Code Block Renderer
Out of the box, Markdown Monster supports the Markdig and Pandoc markdown parsers. Its default being the Markdig parser. To create your own custom markdown parser, you create an add-in and override its GetMarkdownParser
method, returning your own custom parser; as demonstrated in the following excerpt:
public override IMarkdownParser GetMarkdownParser()
{
return new MarkdownParserCodeProject();
}
I wasn't familiar with Markdig until I began work on this add-in. I downloaded the source code from GitHub and browsed through it. It's well structured. Markdig uses a set of renderers that handle turning various markdown expressions into HTML. I determined that I could replace the default Markdig CodeBlockRenderer
with my own. (See Listing 1.)
The CreateRenderer
method in the base MarkdownParserMarkdig
class creates a Markdig HtmlRenderer
instance. In addition to returning an HtmlRenderer
we remove the CodeBlockRenderer
and replace it with an instance of our custom CPCodeBlockRenderer
.
Listing 1. MarkdownParserCodeProject class
class MarkdownParserCodeProject : MarkdownParserMarkdig
{
public MarkdownParserCodeProject(bool pragmaLines = false, bool forceLoad = false)
: base(pragmaLines, forceLoad)
{
}
protected override IMarkdownRenderer CreateRenderer(TextWriter writer)
{
var renderer = new HtmlRenderer(writer);
CodeBlockRenderer codeBlockRenderer = null;
foreach (var objectRenderer in renderer.ObjectRenderers)
{
codeBlockRenderer = objectRenderer as CodeBlockRenderer;
if (codeBlockRenderer != null)
{
break;
}
}
var cpCodeBlockRenderer = new CPCodeBlockRenderer();
if (codeBlockRenderer != null)
{
renderer.ObjectRenderers.Replace<CodeBlockRenderer>(cpCodeBlockRenderer);
}
else
{
renderer.ObjectRenderers.Add(cpCodeBlockRenderer);
}
return renderer;
}
}
The custom CodeBlockRenderer
outputs pre
tags without wrapping the content in code
elements. (See Listing 2.)
When the CodeBlock
object is used for a fenced code block that has a programming language specified, the CodeBlock
object's attributes indicate the name of the CSS class. The CSS class might be, for example, 'language-xml'. This CSS class name must be mapped to a corresponding value for the lang
attribute of CodeProject's pre
tag.
For my purposes, my priority was C# and XML. I haven't tested that the rest of the value's map correctly. If you find one that doesn't, let me know and I'll update the add-in.
Listing 2. CPCodeBlockRenderer class
public class CPCodeBlockRenderer : CodeBlockRenderer
{
protected override void Write(HtmlRenderer renderer, CodeBlock obj)
{
renderer.EnsureLine();
renderer.Write("<pre");
var attributes = obj.TryGetAttributes();
string cssClass = attributes?.Classes.FirstOrDefault();
if (cssClass != null)
{
string langAttributeValue = TranslateCodeClass(cssClass);
renderer.Write(" lang=\"");
renderer.WriteEscape(langAttributeValue);
renderer.Write("\" ");
}
if (attributes?.Id != null)
{
renderer.Write(" id=\"").WriteEscape(attributes.Id).Write("\" ");
}
renderer.Write(">");
renderer.WriteLeafRawLines(obj, true, true);
renderer.WriteLine("</pre>");
}
string TranslateCodeClass(string cssClass)
{
string result;
if (!langLookup.TryGetValue(cssClass, out result))
{
const string languagePrefix = "language-";
if (cssClass.StartsWith(languagePrefix))
{
result = cssClass.Substring(languagePrefix.Length);
}
}
return result;
}
Dictionary<string, string> langLookup = new Dictionary<string, string>
{
{"language-csharp", "cs"},
{"language-javascript", "jscript"},
...
};
}
Copying the HTML to the Clipboard
We could be done now, but extracting the HTML from the preview is tedious. You need to either view and copy the source from the preview pane or browser window, and then copy the pertinent section within the body element. I decided to create a button to grab only the HTML you need, so you can immediately paste it into your CodeProject article.
Markdown Monster allows you to add toolbar items and drop down menus to its interface. It also comes with built-in support for Font Awesome, so there's no messing about creating images for toolbar items.
To add a toolbar item to Markdown Monster, create an AddInMenuItem
and add it to the add-in class's MenuItems
collection. (See Listing 3.)
Listing 3. CodeProjectMarkdownParserAddin.OnApplicationStart method
public override void OnApplicationStart()
{
base.OnApplicationStart();
Id = "CodeProjectMarkdownParserAddin";
Name = "CodeProject Markdown Parser";
AddInMenuItem menuItem = new AddInMenuItem(this)
{
Caption = "Copy HTML to Clipboard",
FontawesomeIcon = FontAwesomeIcon.Clipboard
};
menuItem.ExecuteConfiguration = null;
MenuItems.Add(menuItem);
EnsureThemeExists();
}
When the button is clicked, the add-in class's OnExecute
method is called. You override OnExecute
to apply your button logic. In this case, the add-in calls the CopyHtmlToClipboard
method, as shown in the following excerpt:
public override void OnExecute(object sender)
{
CopyHtmlToClipboard();
}
The CopyHtmlToClipboard
method calls the RenderHtml
method of the currently active document. That produces a string of HTML representing only the content of the markdown document; no html
, head
, or body
tags. (See Listing 4.)
The text is copied to the Clipboard using its static SetText
method.
A confirmation that the text has been copied is displayed in Markdown Monster's status bar for 3 seconds using the base class's ShowStatus
method.
Listing 4. CodeProjectMarkdownParserAddin.CopyHtmlToClipboard method
void CopyHtmlToClipboard()
{
MarkdownDocument document = ActiveDocument;
string html = document.RenderHtml();
Clipboard.SetText(html);
ShowStatus("HTML copied to clipboard.", 3000);
}
Creating a Custom Theme
To give you some sense of how the HTML will look when you paste it into your CodeProject article, I created a custom preview theme. To do this I downloaded CodeProject's main CSS file and placed it into a new directory, named CodeProject, in Markdown Monster's PreviewThemes directory.
I then created a Theme.html document in the same directory. (See Listing 5.)
I import the CodeProject CSS by adding a link to {$themePath}CodeProject_Main.min.css
. {$themePath}
resolves to the theme's directory at run-time.
Markdown Monster replaces {$markdownHtml}
with the rendered HTML.
The divisions surrounding {$markdownHtml}
emulate the CodeProject submission wizard's article preview page.
Listing 5. Theme.html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<base href="{$docPath}" />
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<link href="{$themePath}CodeProject_Main.min.css" rel="stylesheet"/>
<script src="{$themePath}..\scripts\jquery.min.js"></script>
<script src="{$themePath}..\scripts\preview.js"></script>
</head>
<body class="edge edge15" style="top: 0px; position: relative; min-height: 100%;">
<div class="container-article fixed" id="AT">
<div class="article">
<div class="text" id="contentdiv">
<!--
{$markdownHtml}
<!--
</div>
</div>
</div>
</body>
</html>
Installing a Theme at Run-Time
Presently, there does not exist a mechanism to include the theme in a Markdown Monster add-in package. It's up to the add-in to ensure the theme exists at run-time.
The way I did this was to include the .html and .css files within the add-in project. I set the content type of the files to Embedded Resource. When the add-in runs, it checks that the theme files have been copied to Markdown Monster's PreviewThemes directory. (See Listing 6.) If not, it extracts the files from the assembly and copies them over.
Listing 6. CodeProjectMarkdownParserAddin.EnsureThemeExists method
void EnsureThemeExists()
{
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
var previewThemesDirectory = Path.Combine(baseDirectory, "PreviewThemes");
if (!Directory.Exists(previewThemesDirectory))
{
return;
}
var cpThemeDir = Path.Combine(previewThemesDirectory, "CodeProject");
if (!Directory.Exists(cpThemeDir))
{
Directory.CreateDirectory(cpThemeDir);
}
string themeFile = Path.Combine(previewThemesDirectory, "Theme.html");
if (!File.Exists(themeFile))
{
CopyEmbeddedResources(cpThemeDir, "CodeProjectMarkdownParserAddin.PreviewTheme",
new List<string> { "CodeProject_Main.min.css", "Theme.html" });
}
}
The CopyEmbeddedResources
method retrieves the actual files from the Assembly
object, using its GetManifestResourceStream
method. (See Listing 7.) The file is then copied to the destination directory.
Listing 7. CodeProjectMarkdownParserAddin.CopyEmbeddedResources method
static void CopyEmbeddedResources(string outputDir, string resourceLocation, List<string> files)
{
var assembly = Assembly.GetExecutingAssembly();
foreach (var file in files)
{
string embeddedResourcePath = resourceLocation + @"." + file;
using (Stream stream = assembly.GetManifestResourceStream(embeddedResourcePath))
{
if (stream == null)
{
throw new Exception("Unable to locate embedded resource " + embeddedResourcePath);
}
string filePath = Path.Combine(outputDir, file);
using (FileStream fileStream = new FileStream(filePath, FileMode.Create))
{
for (var i = 0; i < stream.Length; i++)
{
fileStream.WriteByte((byte)stream.ReadByte());
}
fileStream.Close();
}
}
}
}
Conclusion
In this article, you saw how to use the CodeProject Markdown Parser add-in. You then explored the add-in's implementation. You looked at creating a custom Markdig code block renderer to correctly generate CodeProject pre
tags from markdown fenced code blocks. You saw how to create a custom theme for the previewer in Markdown Monster and at how to install the theme to Markdown Monster's theme folder.
I hope you find this project useful. If so, then I'd appreciate it if you would rate it and/or leave feedback below. This will help me to make my next article better.
History
- 2017/08/28 First published.