Eddie Flores

Product Development & Engineering

Adding Syntax Highlighting to Blackfriday

I'm working on the last phase of RacerFive (R5), an SEO focused site engine built in Go, and it's time for me to add syntax highlighting. These are the packages we'll use:

  • Blackfriday - A markdown processor for Go.
  • Chroma - A syntax highlighter built in Go.

Blackfriday allows for annotated codeblocks, which means that a programming language can be specified to identify its code content.

This is the result of an annotated JavaScript snippet:

// add returns the sum of arguments a and b.
function add(a, b) {
  return a + b;
}

It's written out like this:

```js
// add returns the sum of arguments a and b.
function add(a, b) {
  return a + b;
}
```

The annotation (in this case, js) conveniently allows us to pass it to Chroma ahead of time—saving us the effort of analyzing the code to identify a language. For situations where no annotation is found, we'll use Chroma's lexers.Analyse as a way to determine the syntax.

Integrating Chroma to Blackfriday

Blackfriday allows for overriding of its default renderer using blackfriday.WithRenderer, which behaves like a unix pipe, allowing renderers to override the results of previous renderers. In this case, we're only interested in overriding the contents of blackfriday.CodeBlock with syntax highlighted code.

To do that, the first step is to implement the blackfriday.Renderer interface.

Defining a Renderer

// ChromeRenderer is a type that implements the blackfriday.Renderer interface
// for syntax highlighting of pre tag content.
type ChromaRenderer struct {
  html  *blackfriday.HTMLRenderer
  theme string
}

// RenderNode is called with the node being traversed.
func (r *ChromaRenderer) RenderNode(w io.Writer, node *blackfriday.Node, 
	entering bool) blackfriday.WalkStatus {
  switch node.Type {
  // We only care about the pre tag.
  case blackfriday.CodeBlock:
    // Set up a lexer.
    var lexer chroma.Lexer

    // Read the language from the annotation.
    lang := string(node.CodeBlockData.Info)
    if lang != "" {
      lexer = lexers.Get(lang)
    } else {
      // Analyze when no language annotation is given.
      lexer = lexers.Analyse(string(node.Literal))
    }

    // If no annotation was found and couldn't be analyzed, fallback.
    if lexer == nil {
      lexer = lexers.Fallback
    }

    // Set a syntax highlighting theme
    style := styles.Get(r.theme)
    if style == nil {
      style = styles.Fallback
    }

    // Apply highlighting with Chroma.
    iterator, err := lexer.Tokenise(nil, string(node.Literal))
    if err != nil {
      panic(err)
    }

    // An HTML formatter for the tokenized results.
    formatter := html.New()

    // Write out the highlighted code to the io.Writer.
    err = formatter.Format(w, style, iterator)
    if err != nil {
      panic(err)
    }

    // Move on to the next node.
    return blackfriday.GoToNext
  }

  // Didn't match the CodeBlock type, render it as is.
  return r.html.RenderNode(w, node, entering)
}

// Leaving these blank to satisfy the Renderer interface, not useful to us.
func (r *ChromaRenderer) RenderHeader(w io.Writer, ast *blackfriday.Node) {}
func (r *ChromaRenderer) RenderFooter(w io.Writer, ast *blackfriday.Node) {}

While we're at it, lets make a constructor function:

// NewChromaRenderer returns a renderer for syntax highlighting using Chroma.
func NewChromaRenderer(theme string) *ChromaRenderer {
  return &ChromaRenderer{
    html:  blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{}),
    theme: theme,
  }
}

Now we can use the renderer by passing it the name of an available style:

cr := NewChromaRenderer("monokai")
html := blackfriday.Run(f.body, blackfriday.WithRenderer(cr))

The result is syntax highlighted code with inlined CSS that looks like this:

<pre style="background-color:#fff">
<span style="color:#008000">// NewChromaRenderer returns a renderer for syntax highlighting using Chroma.
</span><span style="color:#008000"></span><span style="color:#00f">func</span> NewChromaRenderer(theme <span style="color:#2b91af">string</span>) *ChromaRenderer {
  <span style="color:#00f">return</span> &amp;ChromaRenderer{
    html:  blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{}),
    theme: theme,
  }
}
</pre>

You can find available styles at the Chroma Style Gallery.

One last thing

If you haven't noticed, the ChromaRenderer was used to syntax highlight the page you just read. I've extracted it out to a Gist that you can find here: ChromaRenderer.go.

For similar content, feel free to follow me on Twitter: @_ef2k. ✌🏽