New Look for northCoder - With My Own Codeblock Style

11 Nov 2024

red sky

This site had been using Even since I first migrated from Blogger to Hugo.

Now, I’m using a much more minimal set-up, almost entirely of my own creation.

One thing that was slightly fiddly was displaying a title bar in my code blocks. For example see the code block below where the title says Go Template in the top left hand corner:

Go Template
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
{{ $langs := dict }}
{{ $path := "chroma/languages.json" }}
{{ with resources.Get $path }}
  {{ with . | transform.Unmarshal }}
    {{ $langs = . }}
  {{ end }}
{{ else }}
  {{ errorf "Unable to get global resource %q" $path }}
{{ end }}

{{ $result := transform.HighlightCodeBlock . }}

{{ if .Type }}
  {{ $lang := index $langs (.Type | lower) }}
  <div class="code-block-container">
    <div class="code-block-language">{{ or $lang .Type }}</div>
    {{ $result.Wrapped }}
  </div>
{{ else }}
  {{ $result.Wrapped }}
{{ end }}

The above code is actually the contents of this file in my Hugo project for this website:

layouts/_default/_markup/render-codeblock.html

So, it’s the exact code I am using in this site to override the default Hugo code block render hook. The original code built into Hugo is the code which handles syntax highlighting using the Chroma library.

The original is very simple:

Go Template
1
2
{{ $result := transform.HighlightCodeBlock . }}
{{ $result.Wrapped }}

Once I had discovered the above two lines of code, it was much easier to add my own title bar to my own renderer.

First, I did the following preparation: I took the list of all the Chroma-supported languages and re-arranged it into the following JSON:

JSON
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  "abap": "ABAP",
  "abnf": "ABNF",
  "as": "ActionScript",
  "actionscript": "ActionScript",
  "as3": "ActionScript 3",
  "actionscript3": "ActionScript 3",
  "ada": "Ada",
  ...
  "yaml": "YAML",
  "yang": "YANG",
  "z80": "Z80 Assembly",
  "zed": "Zed",
  "zig": "Zig"
}

I placed this JSON in a new file: assets/chroma/languages.json.

I then used this file in my render-codeblock.html code (exactly as shown earlier) to look up the language name from the code provided in my Markdown code block (immediately following the three opening backticks).

The first section of my render-codeblock.html code loads the JSON file into a Go dictionary ($langs):

Go Template
1
2
3
4
5
6
7
8
9
{{ $langs := dict }}
{{ $path := "chroma/languages.json" }}
{{ with resources.Get $path }}
  {{ with . | transform.Unmarshal }}
    {{ $langs = . }}
  {{ end }}
{{ else }}
  {{ errorf "Unable to get global resource %q" $path }}
{{ end }}

See also:

Then, the code performs the standard code highlighting process:

1
{{ $result := transform.HighlightCodeBlock . }}

Finally, if there is a .Type provided in the markdown code block, an index lookup is performed using that dictionary:

1
{{ $lang := index $langs (.Type | lower) }}

The .Type variable can be anything you want - it’s the first piece of text immediately following the opening triple-backticks in your code block. Typically it is the coding language being used in the code block.

This gives me a nicely formatted programming language name - for example, Java not java, or Transact-SQL instead of tsql. (If the value of .Type doesn’t exist in my languages JSON file, then it’s just displayed as-is, by using {{ or $lang .Type }}.)

I have set up a Chroma style in my config file:

TOML
1
2
3
4
5
[markup]
  [markup.highlight]
    style = 'solarized-light'
    guessSyntax = true
    lineNos = true

And the rest is handled by some extra CSS I have added to format my specific code block heading.

tip
You can also enhance my approach by providing additional instructions (options) in your codeblock markdown to highlight specific lines of code, and to provide anchor links to lines of code.

I have seen more complicated approaches for solving this problem in other styles (e.g. using CSS ::after to add a code language name in a heading). And of course, there may be simpler ways than my approach, but this works well for my needs.