I recently decided to migrate my blog platform from Hugo to my own home-grown solution. Hugo's great, but I want something simpler and under my control. My own blogging platform/generator is only ~300 lines of code and I intend to keep it that way.

One snag with my change is that, even though my source markdown/frontmatter files weren't changing, the generated HTML would. The reason is that markdown processors and rendering/templating engines can generate subtle (or not-so-subtle) different outputs from the same source markdown.

For example, see this block of text.

I'm a 15" computer.

I say, "Here we go!", you...

```
Some code!
```

Hugo would convert that markdown like so.

<p>I&rsquo;m a 15&quot; computer.</p>
<p>I say, &ldquo;Here we go!&rdquo;, you&hellip;</p>
<pre><code>Some code!
</code></pre>

My new system's rendering engine would convert that markdown differently like so.

<p>I&#39;m a 15&quot; computer.</p>
<p>I say, &quot;Here we go!&quot;, you...</p>
<pre><code>Some code!</code></pre>

Why is the HTML different? Ambiguity exists in all specifications, including markdown. Implementations often vary even if the same rules are followed. I don't know or care if one solution is more compliant than the other. What I care about is usability and the fact that changing markdown engines could result in changes to the compiled HTML, and also may break things in some cases!

To reduce the noise in migrating blogging platforms I wanted to stagger the rollout. I would swap out most of the tooling and templating, but I wanted the markdown translation to be identical so that it would be easier to spot changes and breakages in the diff.

My version of hugo was outdated at 0.71.0. I downloaded that release and built it locally. I added a lot of logging and debug code to figure out how my old Hugo markdown generation worked.

Then, I stripped out the markdown rendering code into my own simple app and built a binary. My new blogging platform could use that binary specifically for the sake of markdown generation. So my new custom blogging platform would still be using the guts of the old Hugo rendering engine.

It took a while to find code like the WithWrapperRenderer() invocation where Hugo had its own specific logic for how to write code tags, but eventually I got my own mini-Hugo engine close enough to real Hugo that I was good to go.

I used my ./markdown binary to generate rendered content and help ease the impact of migrating to a new templating system.

package main

import (
    "bytes"
    "fmt"
    "github.com/alecthomas/chroma/formatters/html"
    "github.com/gohugoio/hugo/markup/highlight"
    "github.com/yuin/goldmark"
    hl "github.com/yuin/goldmark-highlighting"
    "github.com/yuin/goldmark/extension"
    "github.com/yuin/goldmark/parser"
    gmhtml "github.com/yuin/goldmark/renderer/html"
    "github.com/yuin/goldmark/util"
    "io/ioutil"
    "os"
)

var (
    build string
)

func main() {

    if len(os.Args) < 2 {
        fmt.Printf("build=%s\n\n", build)
        panic("Must pass path to a file as the only argument")
    }

    path := os.Args[1]

    source, err := ioutil.ReadFile(path)
    if err != nil {
        panic(err)
    }

    gm := goldmark.New(
        goldmark.WithRendererOptions(
            gmhtml.WithUnsafe(),
        ),
        goldmark.WithParserOptions(
            parser.WithAutoHeadingID(),
        ),
        goldmark.WithExtensions(
            extension.NewTable(),
            newHighlighting(),
            extension.Linkify,
            extension.Typographer,
        ),
    )

    var buf bytes.Buffer
    if err := gm.Convert(source, &buf); err != nil {
        panic(err)
    } else {
        fmt.Println(buf.String())
    }
}

func newHighlighting() goldmark.Extender {
    return hl.NewHighlighting(
        hl.WithStyle("monokai"),
        hl.WithGuessLanguage(false),
        hl.WithCodeBlockOptions(highlight.GetCodeBlockOptions()),
        hl.WithFormatOptions(
            html.TabWidth(4),
            html.WithLineNumbers(false),
            html.BaseLineNumber(1),
            html.LineNumbersInTable(true),
            html.WithClasses(false),
        ),

        hl.WithWrapperRenderer(func(w util.BufWriter, ctx hl.CodeBlockContext, entering bool) {
            l, hasLang := ctx.Language()
            var language string
            if hasLang {
                language = string(l)
            }

            if entering {
                if !ctx.Highlighted() {
                    w.WriteString(`<pre>`)
                    highlight.WriteCodeTag(w, language)
                    return
                }
                w.WriteString(`<div class="highlight">`)
                return
            }

            if !ctx.Highlighted() {
                w.WriteString(`</code></pre>`)
                return
            }

            w.WriteString("</div>")
        }),
    )
}
module markdown

go 1.15

require (
    github.com/alecthomas/chroma v0.8.1
    github.com/gohugoio/hugo v0.76.3
    github.com/yuin/goldmark v1.2.1
    github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691
)
go mod tidy && go build -ldflags "-X 'main.build=$(date)'"

This was only a stop-gap solution to ease the transition. Once things were stable enough and I felt comfortable that markdown was being generated properly (or close enough) I ditched my hacky ./markdown engine for a new off-the-shelf library that better suited my blogging platform.

In hindsight, maybe spending a couple days to carefully test and review the site would've been time better spent than pulling out this rendering code, but what's the fun in that!