Hugo Theme (Part 3) : Syntax Highlighting

In this post, we will add syntax highlighting. We will create colors for light and dark mode. We will also add support for sample code output inside code blocks.

Code block

We need to generate color styles for code blocks.

Light mode

hugo gen chromastyles --style=monokailight > themes/jack/assets/css/syntax.css

In the assets/css/syntax.css, surround the generated styles with

:not(.dark) > body { ... }

For dark mode

hugo gen chromastyles --style=monokai >> themes/jack/assets/css/syntax.css

In the assets/css/syntax.css, surround the generated styles with

.dark { ... }

Link the generated file to our template

In layouts/partials/head/css.html

{{- with resources.Get "css/syntax.css" }}
  {{- if eq hugo.Environment "development" }}
    <link rel="stylesheet" href="{{ .RelPermalink }}">
  {{- else }}
    {{- with . | minify | fingerprint }}
      <link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
    {{- end }}
  {{- end }}
{{- end }}

Tailwind prose class adds some unneeded margins and paddings to code blocks when using highlight shortcode. Let’s add a quick fix for that

.chroma .lnt, .chroma .hl {
    display: flex;
}

.highlight .lntd:first-child > pre {
    @apply my-0 pr-0;
}

.highlight .lntd:nth-child(2) > pre {
    @apply my-0 pl-0;
}

Inline code block

:not(pre) > code {
    @apply bg-white dark:bg-slate-800 italic px-1;
}

Sample output

We can override the default code block renderer with our own to enable support for sample output in code blocks. In the code block we can find all the nested sample code blocks and replace them with their hash before sending them to be rendered by Goldmark renderer. We can then substitute the sample output text back in.

[!Warning]

This code block passes the sample output text through safeHTML function which should not be used for content from untrusted sources.

Create layouts/_default/_markup/render-codeblock.html

{{ $class := .Attributes.class | default ""    }}
{{ $lang  := .Attributes.lang  | default .Type }}
{{ $Inner := .Inner }}
{{ if transform.CanHighlight $lang }}
    {{/*  Find all the samp blocks, capture them and their contents  */}}
    {{ $blockmap := dict }}
    {{ $blocks := findRESubmatch "((\\s|\\S)*?)" $Inner }}
    {{ range $blocks }}
        {{/*  Add all contents and their hashes to dictionary  */}}
        {{ $content := trim (index . 1) "\n\r" }}
        {{ $hash := crypto.FNV32a $content | string }}
        {{ $blockmap = $blockmap | merge (dict $hash $content) }}
        {{/*  Replace all blocks by hashes  */}}
        {{ $Inner = replace $Inner (index . 0) $hash }}
    {{ end }}

    {{/*  Render the code blocks  */}}
    {{ $rendered := highlight $Inner $lang }}

    {{/*  Remove hashes and add original content back  */}}
    {{ range $hash, $content := $blockmap }}
        {{/*  {{ $tmp := highlight $content "text" }}  */}}
        {{ $tmp := partial "samp.html" (dict "content" $content) }}
        {{ $rendered = replace $rendered $hash $tmp}}
    {{ end }}
<div class="{{ $class }}">{{ $rendered | safeHTML }}</div>
{{else}}
<pre><code class="{{ $class }}">{{.Inner}}</code></pre>
{{end}}

In layouts/partials/samp.html

<samp class="text-gray-500 dark:text-gray-400">{{ .content }}</samp>

Now we can use it in markdown like

```js
console.log("hello")
```samp
hello
```
```

and it will render

console.log("hello")
hello

Thank you for reading. Check out the other parts in the series below.