Posted in:

A year ago I rewrote this blog in ASP.NET Core. One of the goals I had was to be able to transition to writing all my posts in Markdown as I wanted to get away from relying on the obsolete Windows Live Writer and simply use VS Code for editing posts.

However, I needed to be able to store each blog post as a Markdown file, and for that I decided to use "YAML front matter" as a way to store metadata such as the post title and categories.

So a the contents of a typical blog post file look something like:

---
title: Welcome!
categories: [ASP.NET Core, C#]
---
Welcome to my new blog! I built it with:

- C#
- ASP.NET Core
- StackOverflow

Parsing YAML Front Matter with YamlDotNet

First of all, to parse the YAML front matter, I used the YamlDotNet NuGet package. It's a little bit fiddly, but you can use the Parser to find the front matter (it comes after a StreamStart and a DocumentStart), and then use an IDeserializer to deserialize the YAML into a suitable class with properties matching the YAML. In my case, the Post class supports setting many properties including the post title, categories, publication date, and even a list of comments, but for my blog I keep things simple and usually only set the title and categories (I use a file name convention to indicate the publication date).

using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;

// ...
var yamlDeserializer = new DeserializerBuilder()
                    .WithNamingConvention(new CamelCaseNamingConvention())
                    .Build();

var text = File.ReadAllText(blogPostMarkdownFile);
using (var input = new StringReader(text))
{
    var parser = new Parser(input);
    parser.Expect<StreamStart>();
    parser.Expect<DocumentStart>();
    var post = yamlDeserializer.Deserialize<Post>(parser);
    parser.Expect<DocumentEnd>();
}

Rendering HTML with MarkDig

To convert the Markdown into HTML, I used the superb MarkDig library. This not only makes it super easy to convert basic Markdown to HTML, but supports several useful extensions. The library author, Alexandre Mutel, is very responsive to pull requests, so I was able to contribute a couple of minor improvements myself to add some features I wanted.

I created a basic MarkdownRenderer class that renders Markdown using the settings I want for my blog. A couple of things of note. First of all, you'll notice in CreateMarkdownPipeline that I've enabled a bunch of helpful extensions that are available out of the box. These gave me pretty much all the support I needed for things like syntax highlighting, tables, embedded YouTube videos, etc. I'm telling it to expect YAML front matter, so I don't need to strip off the YAML before passing it to the renderer. I needed to add a missing mime type, so I've shown how that can be done, even though it's included now. And the most hacky thing I needed to do was to ensure that generated tables had a specific class I wanted to be present for my CSS styling to work properly (I guess there may be an easier way to achieve this now).

Once the MarkdownPipeline has been constructed, we use a MarkdownParser in conjunction with a HtmlRenderer to parse the Markdown and then render it as HTML. One of the features I contributed to MarkDig was the ability to turn relative links into absolute ones. This is needed for my RSS feed, which needs to use absolute links, while my posts just use relative ones.

Here's the code for my MarkdownRenderer which you can adapt for your own needs:

using Markdig;
using Markdig.Syntax;
using Markdig.Renderers.Html;
using Markdig.Extensions.MediaLinks;
using Markdig.Parsers;
using Markdig.Renderers;

// ...

public class MarkdownRenderer
{
    private readonly MarkdownPipeline pipeline;
    public MarkdownRenderer()
    {
        pipeline = CreateMarkdownPipeline();
    }

    public string Render(string markdown, bool absolute)
    {
        var writer = new StringWriter();
        var renderer = new HtmlRenderer(writer);
        if(absolute) renderer.BaseUrl = new Uri("https://markheath.net");
        pipeline.Setup(renderer);

        var document = MarkdownParser.Parse(markdown, pipeline);
        renderer.Render(document);
        writer.Flush();

        return writer.ToString();
    }

    private static MarkdownPipeline CreateMarkdownPipeline()
    {
        var builder = new MarkdownPipelineBuilder()
            .UseYamlFrontMatter()
            .UseCustomContainers()
            .UseEmphasisExtras()
            .UseGridTables()
            .UseMediaLinks()
            .UsePipeTables()
            .UseGenericAttributes(); // Must be last as it is one parser that is modifying other parsers

        var me = builder.Extensions.OfType<MediaLinkExtension>().Single();
        me.Options.ExtensionToMimeType[".mp3"] = "audio/mpeg"; // was missing (should be in the latest version now though)
        builder.DocumentProcessed += document => {
            foreach(var node in document.Descendants())
            {
                if (node is Markdig.Syntax.Block)
                {
                    if (node is Markdig.Extensions.Tables.Table)
                    {
                        node.GetAttributes().AddClass("md-table");
                    }
                }
            }
        };
        return builder.Build();
    }
}

Comments

Comment by Fakhrulhilal Maktum

Do you have ready to use parser? You can share by creating .net tool and let the others use it in CI build.

Fakhrulhilal Maktum