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

rectcircle 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

looeee 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 -}}
@AllanChain

This comment has been minimized.

Copy link

AllanChain commented Dec 31, 2019

But @Iooeee 's code rendered too many end tags 😂

I have investigated into it and wrote a blog post

If the blog is too abstract, here is the code

{{- $headers := findRE "<h[1-4].*?>(.|\n])+?</h[1-4]>" .Content -}}
{{- $has_headers := ge (len $headers) 1 -}}
{{- if $has_headers -}}

{{- $largest := 6 -}}
{{- range $headers -}}
  {{- $headerLevel := index (findRE "[1-4]" . 1) 0 -}}
  {{- $headerLevel := len (seq $headerLevel) -}}
  {{- if lt $headerLevel $largest -}}
    {{- $largest = $headerLevel -}}
  {{- end -}}
{{- end -}}

{{- $firstHeaderLevel := len (seq (index (findRE "[1-4]" (index $headers 0) 1) 0)) -}}

{{- $.Scratch.Set "bareul" slice -}}
<div id="TableOfContents">
<ul>
  {{- range seq (sub $firstHeaderLevel $largest) -}}
    <ul>
    {{- $.Scratch.Add "bareul" (sub (add $largest .) 1) -}}
  {{- end -}}
  {{- range $i, $header := $headers -}}
    {{- $headerLevel := index (findRE "[1-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=\"" "") "\"" "" }}
    {{- $header := replaceRE "<h[1-4].*?>((.|\n])+?)</h[1-4]>" "$1" $header -}}

    {{- if ne $i 0 -}}
      {{- $prevHeaderLevel := index (findRE "[1-4]" (index $headers (sub $i 1)) 1) 0 -}}
      {{- $prevHeaderLevel := len (seq $prevHeaderLevel) -}}
        {{- if gt $headerLevel $prevHeaderLevel -}}
          {{- range seq $prevHeaderLevel (sub $headerLevel 1) -}}
            <ul>
            {{/* the first should not be recorded */}}
            {{- if ne $prevHeaderLevel . -}}
              {{- $.Scratch.Add "bareul" . -}}
            {{- end -}}
          {{- end -}}
        {{- else -}}
          </li>
          {{- if lt $headerLevel $prevHeaderLevel -}}
            {{- range seq (sub $prevHeaderLevel 1) -1 $headerLevel -}}
              {{- if in ($.Scratch.Get "bareul") . -}}
                </ul>
                {{/* manually do pop item */}}
                {{- $tmp := $.Scratch.Get "bareul" -}}
                {{- $.Scratch.Delete "bareul" -}}
                {{- $.Scratch.Set "bareul" slice}}
                {{- range seq (sub (len $tmp) 1) -}}
                  {{- $.Scratch.Add "bareul" (index $tmp (sub . 1)) -}}
                {{- end -}}
              {{- else -}}
                </ul></li>
              {{- end -}}
            {{- end -}}
          {{- end -}}
        {{- end -}}
        <li>
          <a href="#{{- $cleanedID  -}}">{{- $header | safeHTML -}}</a>
    {{- else -}}
    <li>
      <a href="#{{- $cleanedID -}}">{{- $header | safeHTML -}}</a>
    {{- end -}}
  {{- end -}}
  <!-- {{- $firstHeaderLevel := len (seq (index (findRE "[1-4]" (index $headers 0) 1) 0)) -}} -->
  {{ $firstHeaderLevel := $largest }}
  {{- $lastHeaderLevel := len (seq (index (findRE "[1-4]" (index $headers (sub (len $headers) 1)) 1) 0)) -}}
  </li>
  {{- range seq (sub $lastHeaderLevel $firstHeaderLevel) -}}
    {{- if in ($.Scratch.Get "bareul") (add . $firstHeaderLevel) -}}
      </ul>
    {{- else -}}
      </ul></li>
    {{- end -}}
  {{- end -}}
</ul>
</div>
{{- 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.