ReedyBear's Blog

Generate a Table of Contents on Bearblog

An Archaeopteryx wrote a post about Generating a ToC, and it looked like a painful process.

So, I wrote some Javascript to create a button on your 'New Post' page that will generate ToC for you.

Also, checkout my other Bearblog Tips

Contents:

  1. Create the button & generate ToC
  2. Styling (CSS)
  3. The Code (Javascript)
  4. Sample Header 1
  5. Sample Header 2

Create the button & generate ToC

  1. Visit https://bearblog.dev/dashboard/customise/
  2. Copy+Paste the below javascript code into the 'Dashboard footer content'
  3. Once you have written a post, click the 'Generate ToC' button, which generates HTML and copies it into your clipboard
  4. Paste it into your post wherever you want it.

Note: There must be at least one blank line between any HTML and any markdown. The generated ToC HTML thus has extra blank lines added before-and after to make it a little more foolproof.

Styling (CSS)

You can use your BearBlog theme to style the table of contents however you like. Here are some selectors (but no styles):

div.toc {} /* wraps the whole thing */
div.toc ol.toc-list {} /* the outer, numbered list */
div.toc ul.toc-list {} /* any sub-lists, not numbered */
div.toc li {} /* list item */
div.toc li a {} /* the link within the list item */
div.toc ol > li > a {} /* numbered links only */
div.toc ul > li > a {} /* non-numbered links only */
div.toc ol > li > ul > li > a {} /* tier 2 links only */

The Code (Javascript)

Heavily commented for non-coders. This goes into your "Dashboard footer content".

You can also view and modify this in jsfiddle.

<!-- Generate Table of Contents -->
<script type="text/javascript">
// create a new button element
const toc_btn = document.createElement('button');
toc_btn.classList.add('rdb_post_restorer');
toc_btn.setAttribute('onclick', "event.preventDefault();generate_toc();");
toc_btn.innerText = 'Generate ToC';
// add the button to your 'New Post' page
document.querySelector('.sticky-controls').appendChild(toc_btn);

/** scan your post text and generate a ToC, copying it to your clipboard */
function generate_toc(){
  // HTML for the start of the ToC list
  var toc = "\n\n"+'<div class="toc">'
      +"\n"+'<h2 class="toc-head">Contents:</h2>'
      +"\n"+'<ol class="toc-list">';

  // get your post's content
  const body = document.querySelector('#body_content').value;
  // find all header occurences
  const headers_flat = body.matchAll(/^#{1,6}/gm);
  // the first header in the ToC should use '##'
  let header_size = 2;
  // 'started' is to make sure we don't add an </li> before any list items
  let started = false;
  // loop over the header occurences and generate html
  for (const match of headers_flat){
    // how many hashtags this header has
    const length = match[0].length;
    // you should not use '#' for section headers. Use `##`
    if (length < 2)continue; 
    // generate indentations for the HTML so it looks nice
    const indent = "    ";
    const adjusted_indent = indent.repeat(length-2); 
    const li_indent = adjusted_indent + indent;
    if (length == header_size && started == true){
      // close a list item if it does not have a nested list
      toc += "</li>";
    } else if (length > header_size) {
        // add a new sub-list for sub-headers
        toc += "\n"+adjusted_indent+"<ul class=\"toc-list\">";
    } else if (length < header_size){
        // end a sub-list, escape into the parent list
        // .repeat allows us to close multiple open sub-lists at once if needed
        toc += "</li>\n"+adjusted_indent+"</ul></li>".repeat(header_size - length);
    } 
    header_size = length;
    started = true;

    // get the full text of the header
    const start_index = match.index + length;
    const end_index = body.indexOf("\n", match.index);
    const header_text = body.substring(start_index, end_index)
      .trim();
    const header_id = slugify_title(header_text);
    // the html for a link to one section below
    toc += "\n"+li_indent+'<li><a href="#'+header_id+'">'+header_text+'</a>';
  }

  const list_closers = "</ul></li>".repeat(header_size-2);
  // close the ToC list HTML
  toc += "</li>\n    "+list_closers+"\n</ol></div>\n\n";

  navigator.clipboard.writeText(toc);
    // debugging code used during development
    //console.log(toc);
    //document.getElementById("sample_table").innerHTML = toc;
}
  /**
   * Convert a header title into a lower-case version with special characters and spaces removed/replaced.
   * 
   * @dirty_title any string
   * @return a slugified string with only lowercase letters
   */
  function slugify_title(dirty_title){
      let clean = dirty_title.toLowerCase();
      clean = clean.replaceAll(/[^a-z0-9\_\- ]/g, '');
      clean = clean.replaceAll(' ', '-');
      clean = clean.replaceAll(/\-+/g, "-");
      return clean;
  }
</script>

Sample Header 1

This whole section is just for testing the ToC generator and showing you what it looks like when un-styled.

blah blah body text body text and what about an ###inline hashtag which should be ignored.

Sub_header 1

so let's hope that an super ## inline hashtag doesn't mess things up

Let's ne--st again!

We're now even deeper in the ToC

Sample Header 2

okay but this one will only have one nested list

Sample header 2 sub 1

testing testign 2 1

Sample header 2 sub 2

testing testing 2 2

#bearblog #code