Fix Hugo Table of Contents

Removal of wrapping HTML tag in JavaScript

Background (TL;DR)

While setting up the new version of Staticman for my demo GitLab pages, I’ve read developers’ documentations, setup guide and some community blog posts so as to come up with my own guide. It’s originated and inspired from a variety of sources, and refined according to hours of testing. Consequently, despite the original intention to keep things simple, I’ve finally come up with a post with over a thousand words.

To pass my ideas in this post to visitors, it’s better that they have an overview of the contents before actually looking into the details. Therefore, a table of contents is nice-to-have feature for this blog.

Problem

Hugo provides a convenient function {{ .TableOfContents }} for this. It converts the headings of the page into an unordered list of anchors. However, the post’s title and subtitle, which are represented as <h1> and <h2>, are taken into account. As a result, the generated table of contents looks awkward.

      • Section 1
        • subsection 1.1
        • subsection 1.2
      • Section 2

This explains Hugo’s issue #1778.

Goal

To generate a table of contents like the one above.

First attempt

In spite of the eleven likes for Micheal Blum’s snippet from layouts/partials/table-of-contents.html, GitLab’s CD informed me of the failure of job 98224023 through email.

From the message, I opened this blog’s bash command list to test the TOC. Surprisingly, the generated links pointed to my post on laptop fan cleaning.

I tried looking at the related HTML template files, from which I couldn’t understand. As I’m not supposed to do testing testing after the term has started, I abandoned this approach.

Second attempt

Ryan Parman rewrote the aforementioned HTML template file and published it as gist a796d66f. However, when I performed the same test again, the same error was reproduced.

Final solution

Thanks to Yihui Xie’s simple JavaScript, I managed to get this fixed, at least, in runtime level.

Single page testing in browser

To understand what his script actually did, before including this in my theme’s footer, I ran some of the lines in the web developer’s console in the browser.

  • ✓: toc = document.getElementById('TableOfContents')
  • ✗: var toc = document.getElementById('TableOfContents')

What does Xie’s script do?

  1. Extract the element with id TableOfContents.
  2. Set variable ul to be the unordered list directly under #TableOfContents.
  3. Proceed only when ul has more than one child. (A TOC usually contains more than one section, so only the top level ul containing one single item will get “peeled off”.)
  4. Verify if ul exists, exit if it doesn’t.
  5. Set variable li to be the first child. It’s supposed to be the first list item.
  6. Run a check if li really represents an <li>.
  7. Extract the only list element’s internal HTML use this to replace its parent’s external HTML.

Xie’s script enabled me to move one step forward toward the real solution.

    • Section 1
      • subsection 1.1
      • subsection 1.2
    • Section 2

The leftmost dot was still there. Nonetheless, the above verbal analysis of his code allowed me to decouple the logic and to adapt it for other blog themes.

An ugly hard code solution is to copy the above lines and rename the second instance of the unordered list as ul2 in the JavaScript—that’s too hard to swallow. I would definitely go for a loop when doing repetitive tasks—that’s what machines are made for.

Adaptations to Xie’s script to Beautiful Hugo

Since ul and li are changing elements during this tag unwrapping process, we wrap the lines containing them with a loop. As the first iteration is run unconditionally, a do-while loop is chosen for this task. Failing any of the if statements would end the process, so we replace return with break. Finally, just put any condition that allows the loop to run.

// Copyright (c) 2017 Yihui Xie & 2018 Vincent Tam under MIT

(function() {
  var toc = document.getElementById('TableOfContents');
  if (!toc) return;
  do {
    var li, ul = toc.querySelector('ul');
    if (ul.childElementCount !== 1) break;
    li = ul.firstElementChild;
    if (li.tagName !== 'LI') break;
    // remove <ul><li></li></ul> where only <ul> only contains one <li>
    ul.outerHTML = li.innerHTML;
  } while (toc.childElementCount >= 1);
})();

Whole site regenerated

After local testing with hugo server -D, I published my changes to the site’s theme. After updating the theme, the problem’s now fixed.

To instantly view the changes on the site’s theme, I have to modify the copy of the theme’s files under the local repo for the site. However, it’s better to commit the changes to another separate repo ~/beautifulhugo so as to make my theme clean. It will be laborious and prone to errors to type out the whole path by hand. A more efficient solution will be posted in the next post.


No comment