Bootstrap Navigation Tabs in Hugo

in   Hugo   ,

One feature of Bootstrap that I like is the tabbable content , which allows the user to create tabs of local content. One common use-case is to show multiple syntax highlighted code blocks that showcase the same problem, and how to achieve it in different languages.

While this can be achieved by writing HTML directly in the content, it is a lot of extra boilerplate content that could be automated, and makes it much easier on the author to focus on the code content. The syntax I use in this blog allows me to create nested shortcodes, and automatically detect the language based on the title of the tab.

Markdown Content
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{{< tabs c python >}}
{{< codetab >}}
#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("Hello World\n");
    return 0;
}
{{< /codetab >}}

{{< codetab >}}
print "Hello World"
{{< /codetab >}}
{{< /tabs >}}

The tabs shortcode generates a top-level navigation list, and sets up the tabs based on the parameters. The code for that is shown below, along with some comments to help you follow along.

tabs.html
 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
<!-- Unique ID for the tabs within the page -->
{{- $guid := printf "tabs-%d" .Ordinal -}}
<!-- Scratchpad register to specially handle the first element in the list -->
{{- .Scratch.Set "first" true -}}

<ul class="nav nav-tabs" id="{{- $guid -}}" role="tablist">
{{- range .Params -}}
<li class="nav-item">
  {{- $entry := lower . -}}
  <!-- Generate the IDs for the <a> and the <div> elements -->
  {{- $tabid := printf "%s-%s-tab" $guid $entry | anchorize -}}
  {{- $entryid := printf "%s-%s" $guid $entry | anchorize -}}
  <a class="nav-link{{ if eq ($.Scratch.Get "first") true }} active{{ end }}"
    id="{{ $tabid }}" data-toggle="tab" href="#{{ $entryid }}" role="tab"
    aria-controls="{{ $entryid }}" aria-selected="{{ $.Scratch.Get "first" }}">
    {{ $entry | title }}
  </a>
  <!-- Reset the scratchpad register, since we're done with the first parameter -->
  {{- if eq ($.Scratch.Get "first") true -}}{{- $.Scratch.Set "first" false -}}{{- end -}}
</li>
{{- end -}}
</ul>

<!-- Inner content - generated by codetab shortcode -->
<div class="tab-content" id="{{- $guid -}}-content">
{{- .Inner -}}
</div>

The usage of a unique ID allows the user to add more than one tabs list on a page. While ideally one would use a random number generator and pass that through some transformation to get a unique ID, Hugo doesn’t have any facilities for generating random numbers in the templates. The best that we can use is to pass a slice of characters to the shuffle function, get the first few characters and join the resulting slice to get a string back. However, we only need to find a unique ID within the page. This is where the .Ordinal function comes in handy. It returns the zero-based index of the shortcode within the parent entity, whether it is a page, or an enclosing shortcode itself. Using that, and the .Parent function lets us create a codetab shortcode that automatically detects the language based on its position within the enclosing tabs shortcode.

codetab.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{{- $index := .Ordinal -}}
<!-- Make sure that we are enclosed within a tabs shortcode block -->
{{- if ne .Parent.Name "tabs" -}}
{{- errorf "codetab must be used within a tabs block" -}}
{{- end -}}

<!-- Generate the unique ID based on the enclosing tabs .Ordinal -->
{{- $guid := printf "tabs-%d" .Parent.Ordinal -}}
<!-- Trim any leading and trailing newlines from .Inner, this avoids
     spurious lines during syntax highlighting -->
{{- $code := trim .Inner "\n" -}}

{{- $entry := .Parent.Get $index -}}
{{- $entry := lower $entry -}}
{{- $lang := default $entry (.Get "lang") -}}
{{- $hloptions := default "" (.Get "highlight") -}}

{{- $tabid := printf "%s-%s-tab" $guid $entry | anchorize -}}
{{- $entryid := printf "%s-%s" $guid $entry | anchorize -}}

<div class="tab-pane fade{{ if eq $index 0 }} show active{{ end }}"
    id="{{ $entryid }}" role="tabpanel" aria-labelled-by="{{ $tabid }}">
{{- highlight $code $lang $hloptions -}}
</div>

The codetab shortcode also allows the author to override the language of the enclosed code, and add higlight options as necessary. Now that we have the necessary shortcodes, we can see the result of our example listed above.

#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("Hello World\n");
    return 0;
}
print "Hello World"