Skip to content

Instantly share code, notes, and snippets.

@pyrrho
Last active August 18, 2019 12:41
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pyrrho/1d77cdb98ba58c7547f2cdb3fb325c62 to your computer and use it in GitHub Desktop.
Save pyrrho/1d77cdb98ba58c7547f2cdb3fb325c62 to your computer and use it in GitHub Desktop.
Another Hugo partial for generating a Table of Contents
## Pangrams Are Fun!
### Pack My Box With Five Dozen Liquor Jugs
#### Pack My Box With Five Dozen Liquor Jugs
##### Pack My Box With Five Dozen Liquor Jugs
###### Pack My Box With Five Dozen Liquor Jugs
## We Can Drop And Raise Heading Levels By More Than One Step!
#### The First Was An H2
#### The Second Third And Fourth Are H4s
#### This Is The Fourth
### This Is An H3
#### Another H4
### And A Couple H3s To Round Us Out
### And A Couple H3s To Round Us Out
## We Can Style Headings As Expected
### Check "This" Out!
### Check 'This' Out!
### Check &This; Out!
### Check &This; Out!
### Check **This** Out!
### Check _This_ Out!
### Check <em>This</em> Out!
### Check ~~This~~ Out!
### Check `This` Out!
### Check < This > Out!
### Check \<This\> Out!
### Check <span style="font-variant:small-caps;">This</span> Out!
## Unicode Emoji ⁉
### Yeah 🚀 They Work 🙌
### 😨 😩 😵
#### When there's no text, the ID is ex; `toc_29`
## Explicit Heading IDs Work Too {#explicit-headings}
### The ID Doesn't Match! {#made-you-look}
### What Happens With Malformed IDs? {#This Is What Happens!}
#### The Written ID Was {\#This Is What Happens!}
#### The Resulting HTML Looks Like `<h3 id="This Is What Happens!">`
### What Happens With Duplicate IDs? {#duplicate}
#### This Is What Happens! {#duplicate}
#### This Is What Happens! {#duplicate}
## [What Happens With Anchored IDs?](#anchored-ids)
### This Is What Happens {#anchored-ids}
Drop in some Lorem Ipsum text to make sure we can see where the anchors land...
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce egestas pharetra nibh, id ultrices elit eleifend quis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Phasellus ultricies nunc ut pharetra venenatis. Vestibulum non pharetra nunc. Nunc feugiat purus ut lorem scelerisque, ut mattis sapien commodo. Praesent rhoncus interdum sapien, vitae imperdiet leo luctus ut. Vestibulum semper lorem sed fermentum luctus. Praesent venenatis ante lorem, eu lobortis lectus varius eget. Nullam a vestibulum tellus. Mauris sagittis, orci ut tincidunt dignissim, magna purus facilisis ligula, sed auctor lorem mauris in arcu. Fusce eget bibendum odio. Maecenas eu neque lacus. Pellentesque imperdiet augue ut feugiat mollis. Nulla congue massa sem, placerat tincidunt sapien molestie quis.
In sit amet urna risus. Nam at felis a erat ornare eleifend ut nec tortor. Aliquam et dolor non justo lobortis lobortis quis rutrum ligula. Integer vel tempus erat. Integer sit amet lacus ipsum. Maecenas euismod dui ex, et rhoncus sapien imperdiet pharetra. Vivamus ultrices finibus varius. Cras id porta quam. In maximus, dolor sed malesuada tristique, lectus velit euismod purus, a tincidunt lacus nibh vitae magna. Vivamus in enim efficitur eros venenatis venenatis sit amet vitae leo. Sed gravida quam auctor libero tristique auctor. Nunc sit amet metus auctor, ultrices urna id, mollis felis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Maecenas sed pulvinar augue, vitae hendrerit nisl.
Pellentesque nibh tellus, mattis nec sagittis nec, ullamcorper sit amet metus. Aenean consequat mi ac augue molestie pellentesque. Vestibulum mollis posuere varius. Phasellus sem erat, blandit at turpis id, tempus consectetur diam. Donec tempus dignissim nibh, eu facilisis ipsum varius et. Donec volutpat iaculis mi condimentum imperdiet. Donec aliquet sollicitudin metus id efficitur. Vivamus cursus sed justo et lobortis. Duis egestas, arcu ac viverra maximus, odio nisi pretium nibh, sed consectetur lorem nisi at urna. Fusce in eros suscipit, ullamcorper turpis sit amet, convallis ante. Donec congue eros a sem gravida mollis eget non lectus. Fusce ac nisl turpis. Pellentesque ut purus vel urna blandit laoreet. Pellentesque faucibus eget mi lacinia auctor.
Nam sit amet mi elit. Sed non efficitur orci. Aliquam id luctus velit, eu bibendum elit. Mauris sapien lectus, porttitor imperdiet accumsan vulputate, tempus mattis libero. Aenean eget nisi convallis, blandit risus id, malesuada odio. Morbi faucibus elementum arcu. Etiam et nisl et ex sagittis porta in condimentum metus. Curabitur convallis maximus ante ac dapibus. Sed sed suscipit ipsum. Nulla placerat, ex interdum tincidunt vulputate, ligula ante lacinia neque, a aliquam est magna et ligula. Vestibulum mattis urna urna, mattis accumsan lectus condimentum at. Fusce eget libero quis ex lacinia convallis. Fusce luctus nunc velit, pharetra suscipit erat scelerisque ut. Phasellus iaculis turpis libero, sit amet consectetur sem posuere quis. Nulla blandit ex non lacus cursus finibus. Etiam venenatis, mauris vel imperdiet finibus, ante neque eleifend risus, at venenatis arcu libero a libero.
<!-- formatted, for great readability -->
<ul class="table-of-contents__h2">
<li><a href="#attribute-scores-modifiers">Attribute Scores &amp; Modifiers</a></li>
<li><a href="#pangrams-are-fun">Pangrams Are Fun!</a>
<ul class="table-of-contents__h3">
<li><a href="#pack-my-box-with-five-dozen-liquor-jugs">Pack My Box With Five Dozen Liquor Jugs</a>
<ul class="table-of-contents__h4">
<li><a href="#pack-my-box-with-five-dozen-liquor-jugs-1">Pack My Box With Five Dozen Liquor Jugs</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#we-can-drop-and-raise-heading-levels-by-more-than-one-step">We Can Drop And Raise Heading Levels By More Than One Step!</a>
<ul class="table-of-contents__h3">
<li>
<ul class="table-of-contents__h4">
<li><a href="#the-first-was-an-h2">The First Was An H2</a></li>
<li><a href="#the-second-third-and-fourth-are-h4s">The Second Third And Fourth Are H4s</a></li>
<li><a href="#this-is-the-fourth">This Is The Fourth</a></li>
</ul>
</li>
<li><a href="#this-is-an-h3">This Is An H3</a>
<ul class="table-of-contents__h4">
<li><a href="#another-h4">Another H4</a></li>
</ul>
</li>
<li><a href="#and-a-couple-h3s-to-round-us-out">And A Couple H3s To Round Us Out</a></li>
<li><a href="#and-a-couple-h3s-to-round-us-out-1">And A Couple H3s To Round Us Out</a></li>
</ul>
</li>
<li><a href="#we-can-style-headings-as-expected">We Can Style Headings As Expected</a>
<ul class="table-of-contents__h3">
<li><a href="#check-this-out">Check &ldquo;This&rdquo; Out!</a></li>
<li><a href="#check-this-out-1">Check &lsquo;This&rsquo; Out!</a></li>
<li><a href="#check-this-out-2">Check &This; Out!</a></li>
<li><a href="#check-amp-this-out">Check &amp;This; Out!</a></li>
<li><a href="#check-this-out-3">Check <strong>This</strong> Out!</a></li>
<li><a href="#check-this-out-4">Check <em>This</em> Out!</a></li>
<li><a href="#check-em-this-em-out">Check <em>This</em> Out!</a></li>
<li><a href="#check-this-out-5">Check <del>This</del> Out!</a></li>
<li><a href="#check-this-out-6">Check <code>This</code> Out!</a></li>
<li><a href="#check-this-out-7">Check &lt; This &gt; Out!</a></li>
<li><a href="#check-this-out-8">Check &lt;This&gt; Out!</a></li>
<li><a href="#check-span-style-font-variant-small-caps-this-span-out">Check <span style="font-variant:small-caps;">This</span> Out!</a></li>
</ul>
</li>
<li><a href="#unicode-emoji">Unicode Emoji ⁉</a>
<ul class="table-of-contents__h3">
<li><a href="#yeah-they-work">Yeah 🚀 They Work 🙌</a></li>
<li><a href="#toc_29">😨 😩 😵</a>
<ul class="table-of-contents__h4">
<li><a href="#when-there-s-no-text-the-id-is-ex-toc-29">When there&rsquo;s no text, the ID is ex; <code>toc_29</code></a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#explicit-headings">Explicit Heading IDs Work Too</a>
<ul class="table-of-contents__h3">
<li><a href="#made-you-look">The ID Doesn&rsquo;t Match!</a></li>
<li><a href="#">What Happens With Malformed IDs?</a>
<ul class="table-of-contents__h4">
<li><a href="#the-written-id-was-this-is-what-happens">The Written ID Was {#This Is What Happens!}</a></li>
<li><a href="#the-resulting-html-looks-like-h3-id-this-is-what-happens">The Resulting HTML Looks Like <code>&lt;h3 id=&quot;This Is What Happens!&quot;&gt;</code></a></li>
</ul>
</li>
<li><a href="#duplicate">What Happens With Duplicate IDs?</a>
<ul class="table-of-contents__h4">
<li><a href="#duplicate-1">This Is What Happens!</a></li>
<li><a href="#duplicate-2">This Is What Happens!</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#what-happens-with-anchored-ids-anchored-ids">What Happens With Anchored IDs?</a>
<ul class="table-of-contents__h3">
<li><a href="#anchored-ids">This Is What Happens</a></li>
</ul>
</li>
</ul>
. . .
<h2 id="pangrams-are-fun">Pangrams Are Fun!</h2>
<h3 id="pack-my-box-with-five-dozen-liquor-jugs">Pack My Box With Five Dozen Liquor Jugs</h3>
<h4 id="pack-my-box-with-five-dozen-liquor-jugs-1">Pack My Box With Five Dozen Liquor Jugs</h4>
<h5 id="pack-my-box-with-five-dozen-liquor-jugs-2">Pack My Box With Five Dozen Liquor Jugs</h5>
<h6 id="pack-my-box-with-five-dozen-liquor-jugs-3">Pack My Box With Five Dozen Liquor Jugs</h6>
<h2 id="we-can-drop-and-raise-heading-levels-by-more-than-one-step">We Can Drop And Raise Heading Levels By More Than One Step!</h2>
<h4 id="the-first-was-an-h2">The First Was An H2</h4>
<h4 id="the-second-third-and-fourth-are-h4s">The Second Third And Fourth Are H4s</h4>
<h4 id="this-is-the-fourth">This Is The Fourth</h4>
<h3 id="this-is-an-h3">This Is An H3</h3>
<h4 id="another-h4">Another H4</h4>
<h3 id="and-a-couple-h3s-to-round-us-out">And A Couple H3s To Round Us Out</h3>
<h3 id="and-a-couple-h3s-to-round-us-out-1">And A Couple H3s To Round Us Out</h3>
<h2 id="we-can-style-headings-as-expected">We Can Style Headings As Expected</h2>
<h3 id="check-this-out">Check &ldquo;This&rdquo; Out!</h3>
<h3 id="check-this-out-1">Check &lsquo;This&rsquo; Out!</h3>
<h3 id="check-this-out-2">Check &This; Out!</h3>
<h3 id="check-amp-this-out">Check &amp;This; Out!</h3>
<h3 id="check-this-out-3">Check <strong>This</strong> Out!</h3>
<h3 id="check-this-out-4">Check <em>This</em> Out!</h3>
<h3 id="check-em-this-em-out">Check <em>This</em> Out!</h3>
<h3 id="check-this-out-5">Check <del>This</del> Out!</h3>
<h3 id="check-this-out-6">Check <code>This</code> Out!</h3>
<h3 id="check-this-out-7">Check &lt; This &gt; Out!</h3>
<h3 id="check-this-out-8">Check &lt;This&gt; Out!</h3>
<h3 id="check-span-style-font-variant-small-caps-this-span-out">Check <span style="font-variant:small-caps;">This</span> Out!</h3>
<h2 id="unicode-emoji">Unicode Emoji ⁉</h2>
<h3 id="yeah-they-work">Yeah 🚀 They Work 🙌</h3>
<h3 id="toc_29">😨 😩 😵</h3>
<h4 id="when-there-s-no-text-the-id-is-ex-toc-29">When there&rsquo;s no text, the ID is ex; <code>toc_29</code></h4>
<h2 id="explicit-headings">Explicit Heading IDs Work Too</h2>
<h3 id="made-you-look">The ID Doesn&rsquo;t Match!</h3>
<h3 id="This Is What Happens!">What Happens With Malformed IDs?</h3>
<h4 id="the-written-id-was-this-is-what-happens">The Written ID Was {#This Is What Happens!}</h4>
<h4 id="the-resulting-html-looks-like-h3-id-this-is-what-happens">The Resulting HTML Looks Like <code>&lt;h3 id=&quot;This Is What Happens!&quot;&gt;</code></h4>
<h3 id="duplicate">What Happens With Duplicate IDs?</h3>
<h4 id="duplicate-1">This Is What Happens!</h4>
<h4 id="duplicate-2">This Is What Happens!</h4>
<h2 id="what-happens-with-anchored-ids-anchored-ids"><a href="#anchored-ids">What Happens With Anchored IDs?</a></h2>
<h3 id="anchored-ids">This Is What Happens</h3>
. . .
{{- partial "table-of-contents.html" . -}}
. . .
{{/* This partial was derived from the conversations in and around
https://github.com/gohugoio/hugo/issues/1778 -- most directly skyzyx's
gist; https://gist.github.com/skyzyx/a796d66f6a124f057f3374eff0b3f99a
*/}}
{{/* Minimum heading level to include; default is h2. */}}
{{- $minLevel := 2 -}}
{{/* Minimum heading level to include; default is h4. */}}
{{- $maxLevel := 4 -}}
{{/* Search for headings as specified by $minLevel and $maxLevel, ignoring those
that contain no text (ex; "<h2></h2>" will be ignored).
*/}}
{{- $regex := printf "<h[%d-%d].*?>(.|\n])+?</h[%d-%d]>" $minLevel $maxLevel $minLevel $maxLevel -}}
{{- $headings := findRE $regex .Content -}}
{{/* Skip generation if there are no (suitable) headings. */}}
{{- $hasHeadings := ge (len $headings) 1 -}}
{{- if $hasHeadings -}}
<nav class="enclosing-block" class="customize-as-needed">
{{- .Scratch.Set "toc__last-level" (sub $minLevel 1) -}}
{{- range $heading := $headings -}}
{{- $headingLevel := substr $heading 2 1 | int -}}
{{- $headingID := index (findRE "id=.([a-z0-9-_])*" $heading) 0 | after 4 -}}
{{- $headingTextRaw := substr (index (findRE ">.*</h" $heading 1) 0) 1 -3 -}}
{{/* There may be an anchor tag wrapping some or all of the heading text.
We don't want those tags in our ToC text, so lets get rid of them. */}}
{{- $headingText := replaceRE "</?a.*?>" "" $headingTextRaw | markdownify }}
{{- $lastLevel := $.Scratch.Get "toc__last-level" -}}
{{- $href := printf "#%s" $headingID -}}
{{- $levelSeq := seq $lastLevel $headingLevel | after 1 -}}
{{- $.Scratch.Set "toc__last-level" $headingLevel -}}
{{- if gt $headingLevel $lastLevel -}}
{{- range $l := $levelSeq -}}
<ul class="table-of-contents__h{{ $l }}"><li>
{{- end -}}
{{- else if lt $headingLevel $lastLevel -}}
{{- range $l := $levelSeq -}}
</li></ul>
{{- end -}}
</li><li>
{{- else -}}
</li><li>
{{- end -}}
<a href="{{ $href }}">{{ $headingText }}</a>
{{- end -}}
{{- range seq ($.Scratch.Get "toc__last-level") (sub $minLevel 1) | after 1 -}}
</li></ul>
{{- end -}}
</nav>
{{- end -}}
@pyrrho
Copy link
Author

pyrrho commented Nov 5, 2018

As part of the discussion in gohugoio/hugo#1778, @xenophenes hit a weird looking error that I'd like to resolve. The error message is as follows,

error calling partial: template: theme/partials/toc.html:28:53: executing "theme/partials/toc.html" at <after 1>: error calling after: no items left

@xenophenes If you have a reproducer for this, I'd love to dig in to figure out what's going on.

@xenophenes
Copy link

Let me look into this a little more and get back to you with more details to debug properly - thanks for working with me on this!

@pyrrho
Copy link
Author

pyrrho commented Nov 21, 2018

I think I've found the cause of the @xenophenes' error.

This script has a baked-in assumption that every heading has an id attribute. Specifically, index (findRE "id=.([a-z0-9-_])*" $heading) 0 will return nil if it's operating on a heading with no ID, and piping nil into after 4 is meaningless -- there are no items after a nil, because a nil is not a container.

If a heading is added to a content page through a shortcode -- let's hypothetically say we added a title.html shortcode that spits out <h1>{{ .Page.Params.title }}</h1> -- the Blackfriday parser won't see that heading, and won't automatically add the ID. The table-of-contents.html partial will see that <h1>, though, and the template engine will explode with an uncaught error.

I chose to simply add an ID to my "hypothetical" title.html partial (<h1 id="_{{ .Page.Params.title | plainify | htmlUnescape | anchorize }}">). Another option would be to modify the $regex such that headings without IDs are not matched against and therefor not added to the table of contents.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment