Experimenting with rendering a Table of Contents

Today I implemented a feature I was thinking about: A ToC on the top of all the articles.

Originally, my idea was to have pelican generate it statically. That would enable the ToC for subscribers of the Atom News Feed too. But it was harder than including a piece of JavaScript in the HTML template and some feed readers might already show their own ToC.

So now let's see how I went about it and create a lot of headings in the process so the generator creates a lot of output.

My quick development process

I want to see the first three levels

Level based approach

My first idea was to have

const toc_root = { level: 1, children: [], parent: undefined, };

Pros

  • Would be able to detect the highest level
  • Very long titles would wrap correctly

Cons

  • Hard to implement
  • Coming back to JavaScript from Rust knowing what is a copy vs. what is a borrow/reference is hard and lead me down a very strange path

CSS based approach

Pros

  • Simple to implement

Cons

  • Long titles could give a reader the wrong impression of the level

The Code

This section will now show up in the ToC:

JavaScript
(function() {
    //! Render a TOC on the top of Blog Articles
    if (is_article() == false) {
        return
    }

    const entry_content = document.querySelector("div.entry-content");

    let toc = '';
    let id = 0;
    for (let heading of entry_content.querySelectorAll("h1,h2,h3")) {
        id += 1;
        const title = heading.innerText;
        const id_string = `${id}`;
        const depth = parseInt(heading.nodeName.substr(1));

        const a = document.createElement("a");
        a.name = id_string;
        heading.appendChild(a);

        toc += `  <a class="toc_level${depth}" href="#${id_string}">${title}</a>\n`;
    }
    if (id === 0) { return }

    toc = `<div class="toc_frame">\n <h1>Table of Contents:</h1>\n <div class="toc_entries">\n${toc} </div>\n</div>`;

    entry_content.innerHTML = toc + entry_content.innerHTML;


    function is_article() {
        const parts = location.pathname.split("/");
        const first = parts[1] || '';
        if (first.length !== 4) { return false }
        const year = parseInt(first, 10);
        return year > 2000 && year < 2100;
    }
})()
CSS
.toc_frame h1 { margin-top: 0; font-size: 1.2rem; }
.toc_frame .toc_entries a { display: block; }
.toc_frame .toc_entries a.toc_level1 { margin-left: 0em; }
.toc_frame .toc_entries a.toc_level2 { margin-left: 1em; }
.toc_frame .toc_entries a.toc_level3 { margin-left: 2em; }

Test headings

Test h2

Test h3

Test h3

Test h2

Test h4

links

social