Click here to Skip to main content
15,867,686 members
Articles / Web Development / HTML

Authoring CodeProject Articles in Markdown

Rate me:
Please Sign up or sign in to vote.
5.00/5 (14 votes)
27 Aug 2017CPOL10 min read 13.4K   92   12   3
Use, and learn how to build, a Markdown Monster add-in that outputs HTML compatible with CodeProject's article submission wizard.

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.

Image 1
Figure 1. Markdown Monster Add-In Manager

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:

XML
<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:

XML
<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.

Image 2
Figure 2. Select CodeProject Markdown Parser

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.

Image 3
Figure 3. Select the CodeProject Theme

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.

Image 4
Figure 4. Copy HTML to Clipboard Toolbar Button

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.

Image 5
Figure 5. Select Source view in the CodeProject Article Editor

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:

C#
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

C#
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

C#
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

C#
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
    };

    // if you don't want to display config or main menu item clear handler
    menuItem.ExecuteConfiguration = null;

    // Must add the menu to the collection to display menu and toolbar items            
    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:

C#
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

C#
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

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">
            <!-- Begin CodeProject HTML -->

            {$markdownHtml}

            <!-- End CodeProject HTML -->
        </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

C#
void EnsureThemeExists()
{
    var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
    var previewThemesDirectory = Path.Combine(baseDirectory, "PreviewThemes");

    if (!Directory.Exists(previewThemesDirectory))
    {
        /* Unable to find preview themes directory. Abort. */
        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

C#
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Engineer
Switzerland Switzerland
Daniel is a former senior engineer in Technology and Research at the Office of the CTO at Microsoft, working on next generation systems.

Previously Daniel was a nine-time Microsoft MVP and co-founder of Outcoder, a Swiss software and consulting company.

Daniel is the author of Windows Phone 8 Unleashed and Windows Phone 7.5 Unleashed, both published by SAMS.

Daniel is the developer behind several acclaimed mobile apps including Surfy Browser for Android and Windows Phone. Daniel is the creator of a number of popular open-source projects, most notably Codon.

Would you like Daniel to bring value to your organisation? Please contact

Blog | Twitter


Xamarin Experts
Windows 10 Experts

Comments and Discussions

 
QuestionI like this a lot Pin
Pete O'Hanlon28-Aug-17 23:32
subeditorPete O'Hanlon28-Aug-17 23:32 
AnswerRe: I like this a lot Pin
Daniel Vaughan28-Aug-17 23:56
Daniel Vaughan28-Aug-17 23:56 
GeneralRe: I like this a lot Pin
Pete O'Hanlon29-Aug-17 0:10
subeditorPete O'Hanlon29-Aug-17 0:10 
Daniel Vaughan wrote:
I wasn't happy about. That was until I discovered I could switch out the Markdig CodeBlockRenderer
It's always good when a cleaner opportunity presents itself. Nice one.
This space for rent

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.