Syntax Highlighting with Chroma in Hugo

10 Sep 2022 | Updated on 15 Dec 2022

Introduction

You can config Hugo to generate highlighted code blocks with Chroma. This post records my experience of switching from highlight.js to Chroma, and implementation of some popular code block features such as adding filename as titles.

You can find the complete Hugo documentation here.

Preview

This markdown

```html {path="layouts/partials/head.html", hl_lines="2-4"}
<head>
    <meta name="author" content="{{ with .Site.Params.author }}{{ . }}{{ end }}" />
    <meta name="description" content="{{if .IsHome }}{{ $.Site.Params.description }}{{ else }}{{ .Description }}{{ end }}" />
    <meta name="theme-color" content="#111111" />
</head>
```

is rendered as

> layouts/partials/head.html
1
2
3
4
5
<head>
    <meta name="author" content="{{ with .Site.Params.author }}{{ . }}{{ end }}" />
    <meta name="description" content="{{if .IsHome }}{{ $.Site.Params.description }}{{ else }}{{ .Description }}{{ end }}" />
    <meta name="theme-color" content="#111111" />
</head>

The block will expand horizontally on hover when the media width is at least 1600px.

Chroma Setup

Chroma is enabled by default, so we can start highlighting codes by either specifying one of the languages Chroma supports in the markdown:

```javascript
function addOne(number) {
    return number + 1;
}
```

or by using the built-in highlight shortcode:

{{< highlight javascript >}}
function addOne(number) {
    return number + 1;
}
{{< /highlight >}}

Using custom CSS

There are many Chroma styles you can choose from, and you can just specify the styles with config parameter markup.highlight.style.

> config/config.yaml
1
2
3
markup:
  highlight:
    style: monokai

You can also use custom stylesheet by turning off the inline CSS feature:

> config/config.yaml
1
2
3
markup:
  highlight:
    noClasses: false

and providing your own CSS file, such as the github-dark theme.

Inline options

You can add inline parameters to code blocks. For example, both markdown blocks:

```javascript {linenos=inline, hl_lines="1 4-6"}
function addOne(number) {
    return number + 1;
}
function addTwo(number) {
    return number + 2;
}
```
{{< highlight javascript "linenos=inline, hl_lines=1 4-6" >}}
function addOne(number) {
    return number + 1;
}
function addTwo(number) {
    return number + 2;
}
{{< /highlight >}}

render as follows.

1function addOne(number) {
2    return number + 1;
3}
4function addTwo(number) {
5    return number + 2;
6}

For the full list of options, visit the Hugo documentation.

Additional Features

Adding titles

The usual approach to add a custom title to a code block is to write a separate shortcode above the code fence.

{{< path "/src/lib/util.js" >}}
```javascript
function addOne(number) {
    return number + 1;
}
```

This is rather cumbersome, so I added a custom markdown render hook so that the path can be specified within the inline option.

> layouts/_default/_markup/render-codeblock.html
1
2
3
4
5
6
7
8
<div class="code-container">
  <div class="code-block">
  {{- with (index .Attributes "path") -}}
    <div class="code-path">&gt; {{ . }}</div>
  {{- end -}}
  {{- highlight .Inner .Type .Options -}}
  </div>
</div>
Explanation

When the markdown is parsed, all valid option parameters are stored in .Options, and the additional parameters are stored in .Attributes and attached as HTML attributes. So if linenos=table, path="path/to/file" is passed as option, we will have

{{ .Attributes := dict "path" "path/to/file" }}
{{ .Options := dict "linenos" "table" }}

and the code block will be rendered as:

<div class="highlight" path="path/to/file">
  <pre tabindex="0" class="chroma">
    <!-- code with line numbers -->
  </pre>
</div>

My strategy is to check if the path key from the map of attributes, and then add a <div class="code-path"> element that contains its value. Ideally, .Attributes need to be fed into the highlighter as well, but I am currently not using custom attributes in my code fences, and manipulating maps in Hugo is still a painful process.

This custom template allows us to provide a path option in code fences. For example, the source code for the code block above looks like this.

```html {path="layouts/_default/_markup/render-codeblock.html"}
<div class="code-container">
  ...
</div>
```