Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Hugo Partial for Generating the Table of Contents
...
{{- partial "toc.html" . -}}
...
{{/* https://github.com/gohugoio/hugo/issues/1778 */}}
{{/* ignore empty links with + */}}
{{- $headers := findRE "<h[2-4].*?>(.|\n])+?</h[2-4]>" .Content -}}
{{ .Scratch.Set "last_level" 1 }}
{{/* at least one header to link to */}}
{{- $has_headers := ge (len $headers) 1 -}}
{{- if $has_headers -}}
<aside class="table-of-contents">
<details>
<summary>
<b>Table of Contents</b>
</summary>
{{- range $headers -}}
{{- $header := . -}}
{{- $base := ($.Page.File.LogicalName) -}}
{{- $anchorId := ($header | plainify | htmlUnescape | anchorize) -}}
{{- $href := delimit (slice $base $anchorId) "#" | string -}}
{{- range findRE "[2-4]" . 1 -}}
{{- $next_heading := (int .) -}}
{{- if gt $next_heading ($.Scratch.Get "last_level") -}}
<ul class="toc-h{{ . }}">
{{- else if lt $next_heading ($.Scratch.Get "last_level") -}}
</ul>
{{- end -}}
<li><a href="{{ relref $.Page $href }}">{{- $header | plainify | htmlUnescape -}}</a></li>
{{ $.Scratch.Set "last_level" $next_heading }}
{{- end -}}
{{- end -}}
</details>
</aside>
{{- end -}}
@rectcircle

This comment has been minimized.

Copy link

commented May 7, 2019

hi, the code snippet has a bug.

while my markdown content headers look like as follow :

## h2

### h3

#### h4

## h2

your partial will output as follow:
image

You can modify it like the following:

{{/* https://github.com/gohugoio/hugo/issues/1778 */}}

{{/* ignore empty links with + */}}
{{- $headers := findRE "<h[2-4].*?>(.|\n])+?</h[2-4]>" .Content -}}
{{ .Scratch.Set "last_level" 1 }}

{{/* at least one header to link to */}}
{{- $has_headers := ge (len $headers) 1 -}}
{{- if $has_headers -}}
<aside class="table-of-contents">
    <details>
    <summary>
    <b>Table of Contents</b>
    </summary>
    {{- range $headers -}}
        {{- $last_level := $.Scratch.Get "last_level" -}}
        {{- $header := . -}}
        {{- $base := ($.Page.File.LogicalName) -}}
        {{- $anchorId := ($header | plainify | htmlUnescape | anchorize) -}}
        {{- $href := delimit (slice $base $anchorId) "#" | string -}}
        {{- range findRE "[2-4]" . 1 -}}
            {{- $next_level := (int .) -}}
            {{- if gt $next_level $last_level -}}
                {{- range seq (add $last_level 1) $next_level}}
                    <ul class="toc-h{{ . }}">
                {{- end -}}
            {{- else if lt $next_level $last_level -}}
                {{- range seq (add $next_level 1) $last_level}}
                    </ul>
                {{- end -}}
            {{- end -}}
            <li><a href="{{ relref $.Page $href }}">{{- $header | plainify | htmlUnescape -}}</a></li>
            {{ $.Scratch.Set "last_level" $next_level }}
        {{- end -}}
    {{- end -}}
    </details>
</aside>
{{- end -}}
@looeee

This comment has been minimized.

Copy link

commented Oct 1, 2019

Neither of these work correctly for me - they both leave <ul> tags unclosed. However I found one one disaev.me that works.

I've edited it so that it works with custom anchors which none of the others do, and removed all extra HTML so it just returns nested <ul>

### some heading {#custom-anchor}
{{- $headers := findRE "<h[2-4].*?>(.|\n])+?</h[2-4]>" .Content -}}
{{- $has_headers := ge (len $headers) 1 -}}
{{- if $has_headers -}}
<ul >
  {{- range $i, $header := $headers -}}
    {{- $headerLevel := index (findRE "[2-4]" . 1) 0 -}}
    {{- $headerLevel := len (seq $headerLevel) -}}

    {{/* get id="xyz" */}}
    {{ $id := index (findRE "(id=\"([^\"]*?)\")" $header 9) 0 }}

    {{/* strip id="" to leave xyz (no way to get regex capturing groups in hugo :( */}}
    {{ $cleanedID := replace (replace $id "id=\"" "") "\"" "" }}

    {{- if ne $i 0 -}}
      {{- $prevHeaderLevel := index (findRE "[2-4]" (index $headers (sub $i 1)) 1) 0 -}}
      {{- $prevHeaderLevel := len (seq $prevHeaderLevel) -}}
        {{- if gt $headerLevel $prevHeaderLevel -}}
          {{- range seq (sub $headerLevel $prevHeaderLevel) -}}
            <ul>
          {{- end}}
        {{- end}}
        {{- if lt $headerLevel $prevHeaderLevel -}}
          {{- range seq (sub $prevHeaderLevel $headerLevel) -}}
            </li></ul></li>
          {{- end}}
        {{- end}}
        {{- if eq $headerLevel $prevHeaderLevel -}}
          </li>
        {{- end}}
        <li>
          <a href="#{{- $cleanedID  -}}">{{- $header | plainify | htmlEscape | safeHTML -}}</a>
        {{- if eq $i (sub (len $headers) 1) -}}
          {{- range seq (sub $prevHeaderLevel $headerLevel) -}}
            </li></ul></li>
          {{- end}}
        {{- end}}
    {{- else}}
    <li>
      <a href="#{{- $cleanedID -}}">{{- $header | plainify | htmlEscape | safeHTML -}}</a>
    {{- end -}}
  {{- end -}}
  {{- $firstHeaderLevel := len (seq (index (findRE "[2-4]" (index $headers 0) 1) 0)) -}}
  {{- $lastHeaderLevel := len (seq (index (findRE "[2-4]" (index $headers (sub (len $headers) 1)) 1) 0)) -}}
  {{- range seq (sub $lastHeaderLevel $firstHeaderLevel) -}}
    </li></ul></li>
  {{- end -}}
</ul>
{{- end -}}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.