Hugo Customization

Hugo Customization

April 21, 2021
post, hugo, shortcodes, javascript, math, latex

Some notes from the process of customizing my Hugo site.

I expect this will mostly be useful to my future self when I’m wondering how I ever got this to work, but maybe you’ll find some useful things herein.

Handling LaTeX Math #

I am frequently amazed by how janky writing math on the Internet is. After a number of experiments, I feel like I have something workable now.

Using KaTeX (or MathJax) without parsing artifacts #

Embedding LaTeX math expressions without any special handling works a good chunk of the time:

$$ a^2 + b^2 = c^2 $$

$$ e^{i \pi} + 1 = 0 $$

However, most Markdown engines will mangle non-trivial expressions. Subscripts become italicized and sometimes you have to double up on your forward slashes, which is annoying on its own, but that irritation is compounded by how it’s no longer feasible to copy-paste math between .md and .tex files.

It would be preferable to extend the syntax, but that’s not easy to do, especially given that Hugo’s parsing is still subject to changes, so you can end up having to maintain a fork like fast.ai does.

One alternative that I looked into was employing a preprocessing step in order to transform documents containing math into pure Commonmark compatible form, but (as far as I can tell) there’s no easy way to hook this in to the Hugo build process, so you lose the benefits of live reloading with hugo server.

For now I’m trying variation on Yihui Xie’s solution, which is to leverage how raw elements (either inline or block) are escaped from further parsing.

So you can write this:

```{=latex}
$$
a^2 + b^2 = c^2
$$

$$
e^{i \pi} + 1 = 0
$$
```

and get this:

$$
a^2 + b^2 = c^2
$$

$$
e^{i \pi} + 1 = 0
$$

It even works with more complicated environments:

$$
\begin{aligned}
    \sum_{n=1}^{\infty} \frac{1}{n} &= \infty
    \\
    \sum_{n=1}^{\infty} \frac{1}{n^{2}} &= \frac{\pi^{2}}{6}
\end{aligned}
$$
$$
\mathbb{E}(X) = \int x d F(x) = \left\{ \begin{aligned} \sum_x x f(x) \; & \text{ if } X \text{ is discrete}
\\ \int x f(x) dx \; & \text{ if } X \text{ is continuous }
\end{aligned} \right.
$$

Inline math was a little trickier and required an ugly regex1, but as far as I can tell it’s working splendidly:

> Euler's identity `$e^{i \pi} + 2 = 0$` came as a surprise to Euler himself primarily because it's rare to come across mathematical constructions that already know your name.

> In contrast, the implications of the Pythagorean theorem terrified Pythagoras, *not* (as commonly believed) because of the existence of irrational numbers (in the real world, you see them all the time) but because `$a^2 + b^2 = c^2$` had to first be translated into greek (*i.e.*, `$α^2 + β^2 = γ^2$`), which also led him to discover the latin alphabet hundreds of years before Rome even came into being.

which gets rendered as:

Euler’s identity $e^{i \pi} + 2 = 0$ came as a surprise to Euler himself primarily because it’s rare to come across mathematical constructions that already know your name.

In contrast, the implications of the Pythagorean theorem terrified Pythagoras, not (as commonly believed) because of the existence of irrational numbers (in the real world, you see them all the time) but because $a^2 + b^2 = c^2$ had to first be translated into greek (i.e., $α^2 + β^2 = γ^2$), which also led him to discover the latin alphabet hundreds of years before Rome even came into being.

Custom Javascript #

Originally I was storing my custom JS in the /static directory. However, I learned that this was no longer the “cool” way of doing things2 and that the new cool way would make my car faster and my ice colder.

Fortunately, it’s actually really easy to be cool. All you need to do is store your JavaScript in the /assets directory in order to make use of Hugo Pipes, which will be used to build, bundle, minify, fingerprint, floss, and varnish your code.

All of that happens in your template files. I think the reasoning behind this is that it allows Hugo to keep track of potentially changing artifacts, rebuilding them as needed, which avoids the need for a separate build step, e.g. when transpiling ES9000 or SASS or LESS or SCSS or whatever else the frontend people have cooked up this week3.

For my fairly simple needs, this is perfect, because it lets me take advantage of minification, bundling, and fingerprinting for subresource integrity without the horrors of WebPack, although I can see how it might become a hassle with more complicated setups.

For example, to include the latex-autorender.js script, I just had to add the following into my head.html template:

{{ $autorender := resources.Get "js/latex-autorender.js" }}
{{ $js := slice $autorender | resources.Concat "js/bundle.js" | resources.Minify }}
{{ $secureJS := $js | resources.Fingerprint "sha512" }}
<script type="text/javascript" src="{{ $secureJS.Permalink }}" integrity="{{ $secureJS.Data.Integrity }}"></script>

I haven’t really leveraged the included ESBuild or the bundling capability, but if I wanted to it’d look something like this:

<!-- Building a module -->
{{ $main := resources.Get "js/someModule/index.js" | js.Build "main.js" }}
{{ $autorender := resources.Get "js/latex-autorender.js" }}
{{ $js := slice $built $autorender | resources.Concat "js/bundle.js" | resources.Minify }}
{{ $secureJS := $js | resources.Fingerprint "sha512" }}
<script type="text/javascript" src="{{ $secureJS.Permalink }}" integrity="{{ $secureJS.Data.Integrity }}"></script>

Implementation #

Basically we just loop over all the <code> tags, with slightly different handling for inline versus block elements. If we find something that should be rendered by KaTeX, we try to do just that, and then replace the element in question by its rendered form.

Click to expand  
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// Default options to use for rendering
// https://katex.org/docs/options.html
const defaultOptions = {
    throwOnError: false,
};


/*  Apply KaTeX rendering to all <code> tags with class `language-{=latex}`. */
document.addEventListener("DOMContentLoaded", function() {

    // Regex for content enclosed like $$...$$
    blockExp = /^\s*\$\$\s*(.+?)\$\$/gms;

    // Regex for content enclosed like $...$, i.e. inline math
    // First, we use a lookbehind so we match text starting with "$" but not "$$":
    //      (?<!\$)\$
    // Then we match anything that isn't a "$" character or escaped like "\$":
    //      ([^\$]|\\\$)+
    // Finally we look for a closing "$" sign that isn't followed by another "$"
    // OR itself escaped as "\$":
    //      \$(?!\$)(?<!\\\$)
    // inlineExp = /^(?<!\$)\$(([^\$]|\\\$)+)\$(?!\$)(?<!\\\$)/gmsu;
    inlineExp = /^(?<!\$)\$(([^\$]|\\\$)+)\$(?!\$)(?<!\\\$)/msu;



    // Find inline code
    for (let elem of document.querySelectorAll('code')) {
        // Handle code blocks specified with `class="language-{=latex}"`
        if (elem.classList.contains(`language-{=latex}`)) {
            // Handle case where <code> tag is contained in <pre>
            if (elem.parentNode.tagName === 'PRE') {
                elem = elem.parentNode;
            }

            // Process the content
            let content = String.fromHtmlEntities(elem.textContent);
            let output = [];
            const matches = content.matchAll(blockExp);
            for (const match of matches) {
                // Render the math expression to an HTML string
                let result = katex.renderToString(match[1], elem, {...defaultOptions, displayMode: true});

                // Embed in <p> and <span> tags like `renderMathInElement` does
                result = "".concat('<p><span class="katex-display">', result, '</span></p>');
                output.push(result);
            }

            // Embed output inside containing <div>
            let container = document.createElement("div");
            container.classList.add("rendered-latex");
            container.innerHTML = output.join("\n");

            // Replace the element with container div
            elem.replaceWith(container);
        } else {
            // Look for inline math elements
            let content = String.fromHtmlEntities(elem.textContent);

            // Ignore <code> tags that don't begin with "$"
            if (!content.startsWith(`$`)){
                continue;
            }

            let match = content.match(inlineExp);
            if (match) {
                let result = katex.renderToString(match[1], elem, {...defaultOptions, displayMode: false});

                // Create element to contain the result
                let container = document.createElement("span");
                container.classList.add("rendered-latex");

                // Set inner HTML to KaTeX output and then replace the <code> element
                container.innerHTML = result;
                elem.replaceWith(container);
            }
        }
    }
});

In the future I think it might be nicer if it were possible to switch between the rendered and the raw versions, perhaps by clicking on the element.

Shortcodes #

Terminal Emulator Emulating Element #

That is, a shortcode that creates something that looks a terminal, for showing the output of commands, instead of just a raw code block. Something like the following:

me@home: ~/foo/bar docker run --rm --gpus all nvidia/cuda:11.0-base nvidia-smi
+-----------------------------------------------------------------------------+ | NVIDIA-SMI 460.39 Driver Version: 460.39 CUDA Version: 11.2 | |-------------------------------+----------------------+----------------------+ | GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. | | | | MIG M. | |===============================+======================+======================| | 0 GeForce GTX TIT... Off | 00000000:01:00.0 On | N/A | | 22% 38C P0 102W / 250W | 4026MiB / 12209MiB | 13% Default | | | | N/A | +-------------------------------+----------------------+----------------------+ +-----------------------------------------------------------------------------+ | Processes: | | GPU GI CI PID Type Process name GPU Memory | | ID ID Usage | |=============================================================================| +-----------------------------------------------------------------------------+

I knew it was possible because I’d seen it before, so I looked on GitHub and found ubuntu-terminal and hugo-macos-terminal which inspired the following:

ls content/posts/hugo-customization/
images index.md

Or, specifying current directory via pwd and the indicator symbol and giving the command with command:

me@home: ~/foo/bar ls content/posts/hugo-customization
images index.md

The Implementation #

You can see the implementation here  
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
{{/* Check if the shortcode has been called before to avoid repeating CSS */}}
{{- if not (.Page.Scratch.Get "terminal-prompt") -}}
<style>
div.terminal-block {
    width: 95%;
    height: auto;
    box-shadow: 2px 4px 10px rgba(0,0,0,0.5);
    margin-left: auto;
    margin-right: auto;
    padding-top: 2px;
    margin-bottom: 1em;

}
.terminal-content {
    display: block;
    unicode-bidi: embed;
    word-wrap: break-word;
    word-break: break-all;
    background: rgba(56, 4, 40, 0.9);
    font-family: 'Ubuntu Mono', monospace;
    height: calc(100% - 30px);
    width: 100%;
    padding-top: 2px;
    margin-top: -1px;
    line-height: 100%;
}
.terminal-prompt {
    display: flex;
    white-space: normal;
    margin-bottom: 0.5em;
}
.terminal-prompt-symbol {
    color: #dddddd;
}
.terminal-prompt-pwd {
    color: #4878c0;
}
.terminal-prompt-user {
    color: #7eda28;
}
.terminal-output {
    white-space: pre-wrap;
}
</style>
{{ "<!--" | safeHTML }}
Included from: {{ .Name }} shortcode
{{ "-->" | safeHTML }}
{{- .Page.Scratch.Set "terminal-prompt" true -}}
{{- end -}}

{{/* The actual prompt */}}
<div class="terminal-block">
    <div class="terminal-content">
    <div class="terminal-prompt">
        {{ with .Get "user" }}<span class="terminal-prompt-user">{{ . }}:</span>{{ end }}
        {{ with .Get "pwd" }}<span class="terminal-prompt-pwd">{{ . }}</span>{{ end }}
        <span class="terminal-prompt-symbol">{{ if .Get "symbol" }}{{ .Get "symbol" }}{{ else }}${{ end }}&nbsp;</span>
        {{ if .Get "command" }}
            <span class="terminal-prompt-command">{{ .Get "command" }}</span>
        {{ else }}
            {{ range first 1 (split ( trim .Inner "\n" ) "\n")}}
            <span class="terminal-prompt-command">{{ printf "%s\n" . }}</span>
            {{ end }}
        {{ end }}
    </div>
    {{ if .Get "command" }}
        <div class="terminal-output">{{- trim .Inner "\n" -}}</div>
    {{ else }}
        <div class="terminal-output">{{ delimit (after 1 (split (trim .Inner "\n" ) "\n" ) ) "\n" }}</div>
    {{ end }}
    </div>
</div>

Most of the lines are CSS styling, using Hugo’s “scratchpad” to ensure that it’s only included once, on an as-needed basis.

This seems like it could be a source of future confusion, so I use another trick to include the name of the shortcode in a comment just after the injected <style> tag. Then in the future I’ll be able to figure out what’s responsible for overriding things without too much confusion.

The actual implementation is a bit rococo. The prompt is excessively customizable, and I allow for either providing the command (e.g., “ls -la”) as a named variable or as the first line of the prompt, which seems a bit excessive. I don’t really have a good justification here besides saying that this is the first shortcode I wrote and I was trying things out.

Other Shortcode Examples #

I learned a lot about the theory and practice of these shortcodes by referring to other people’s code. If you’re interested in that sorta thing, maybe take a look at the following::

Not Currently Functional #

I’ve tried some other things that haven’t yet worked out, but maybe I’ll get them running in the future.

Extending goldmark #

Originally I tried to fix my rendering woes by extending the Goldmark parser.

Ultimately I would like something as customized as my Pandoc setup4: I’m not fluent in Go, but how hard could it be5? To test the waters I thought I’d start off by adding the dollar-sign delimiters to the syntax.

There were existing projects I could use as a reference, and even some pull requests that hooked those projects into Hugo:

As I mentioned, there are already pull requests towards this end, so assuming you have the main Hugo repo as a remote named upstream, you can check it out via:

1
2
git fetch upstream pull/7435/head:pr-7435
git checkout pr-7435

And then just use go install --tags extended to rebuild Hugo (see the docs for more info).

Unfortunately, this causes you to travel back in time, which can break things, which made it a bit of a non-starter ultimately. The version of Hugo these PRs are based on is missing some features that my site needs, so I actually can’t build it to test things out when

A nifty GitHub feature #

Trying to get it to work nonetheless taught me a few things.

For example, I learned that it’s possible to get some alternate views (as a diff or a patch) via appending .diff or .patch to the pull request URL.

1
curl https://github.com/gohugoio/hugo/pull/7435.diff

If I’d actually managed to get something usable, this would’ve been really helpful for showing what sort of modifications actually needed to be made, or perhaps even applying them directly via git apply. Alas.

References #


  1. The cheap joke would be “but I repeat myself”, although there’s a kernel of truth there—an objectively beautiful regex is somewhat rare. ↩︎

  2. Anyone who works with JavaScript quickly gets used to that particular revelation. Case in point: the naming convention for the language itself↩︎

  3. You could also conceivably use this as a way of including different resources or whatever, but I’m not sure if that would be broadly useful. ↩︎

  4. An example of which you can see in my makedown repo. Recently I’ve been writing in a combination of Markdown and LaTeX, where I use Markdown for most things, but can write blocks with the {=latex} info-string to tell Pandoc that inside that block it’s all LaTeX. Ultimately it ends up generating a single .tex file which is then compiled into a PDF using xelatex.

    Other people have gone further, however, doing stuff like running code for generating graphs and such within those blocks. Ultimately I’d like to get something like pandoc-plot up and running, which would let me make graphs and whatnot without having to switch programs. ↩︎

  5. I’m not sure if there’s a causal relationship between thinking this ought to be fairly simple and the subsequent suffering, but I’m beginning to suspect that the whole hubris/nemesis thingy might not just apply to Greek drama. ↩︎

Generated Wed, 05 May 2021 23:10:04 MDT