Skip to content

Instantly share code, notes, and snippets.

@skyzyx
Last active February 27, 2023 04:21
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save skyzyx/a796d66f6a124f057f3374eff0b3f99a to your computer and use it in GitHub Desktop.
Save skyzyx/a796d66f6a124f057f3374eff0b3f99a to your computer and use it in GitHub Desktop.
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 -}}
@looeee
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
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