snippet

generateTOC

const generateTOC = (fileString) => {
  const headingLst = getHeadings(fileString);

  let tocTree = [];

  for (let i = 0; i < headingLst.length; i++) {
    const currHeading = headingLst[i].replace(/#/g, "").trim().toLowerCase();
    const currLevel = headingLst[i].match(/#/g)?.length || 1;
    const currSlug = currHeading
      .replace(/[^a-z0-9 ]/g, "")
      .replace(/[ ]/g, "-");

    generateTocNode(tocTree, currHeading, currLevel, currSlug);
  }

  return tocTree;
};

The hook situation

Picture the following situation... You've just finished building your blog and you've loaded it with content only to realize that it's missing a table of contents. This is an important user feature as it makes navigating the article easier but you may be wondering how you would implement such a feature.

This helper function allows you to generate a table of contents in the form of a tree -- this is represented as an array of objects. In your front-end code, you could recursively traverse the tree and render out the components to build a visual table of contents.

The Main Objective

The main objective of this helper function is to extract all of the headings from a markdown file string and store the information in a tree structure. It is still left up to you to choose how you want to render the table of contents.

Show more

How does the generateTOC function work?

The generateTOC function relies on two helper functions to work correctly

  1. The getHeadings function
  2. generateTocNode function

The getHeadings function

const getHeadings = (fileString) => {
  const regex = /#{2}.+\n/g;
  const headings = fileString.match(regex) || [];

  return headings;
};

The function takes in a file string and uses regex to find all the headings in the file string. It matches any element that begins with the # character and ends with the new line character \n. It does this for the entire string and returns all the founds elements in a string array.

The generateTocNode Function

const generateTocNode = (subTree, currHeading, currLevel, currSlug) => {
  if (subTree.length === 0) {
    subTree.push({
      name: currHeading,
      children: [],
      level: currLevel,
      slug: currSlug,
    });

    return;
  }

  if (currLevel > subTree[subTree.length - 1].level) {
    generateTocNode(
      subTree[subTree.length - 1].children,
      currHeading,
      currLevel,
      currSlug
    );
  } else {
    subTree.push({
      name: currHeading,
      children: [],
      level: currLevel,
      slug: currSlug,
    });

    return;
  }
};

This function takes in a sub-tree, the current heading, the current slug, and the current level of the header semantic hierarchy. Since this is a recursive function, it first needs to establish a base case --this is done by checking the length of the sub-tree as its subTree is an array. If we find that the length of the subTree is 0 then we are going to append the following object to the subTree.

If the length of the subTree is not 0 (subTree is not empty), then we are going to compare the current level (the level that was passed in) to the level of the node in the subTree. If the current level is greater than the node level in the subTree then we are going to call the generateTOC function again. In the case that the current level is not greater than the current node level, we append the following object to the subTree array.

In the end, we turn an input string into a tree data structure which can be used to build a table of contents.

We go from:

const inputString = `## Heading 2 \n sdrfviodfvnnn dfvnjj dfivn dfjn \n ### Heading 3 -1  \n  sdrfviodfvnnn dfvnjj dfivn dfjn \n ### Heading 3-2 \n #### heading 4 \n sdrfviodfvnnn dfvnjj dfivn dfjn \n ## heading 2`;

to:

const output = [
  {
    name: "heading 2",
    children: [
      { name: "heading 3 -1", children: [], level: 3, slug: "heading-3-1" },
      {
        name: "heading 3-2",
        children: [
          { name: "heading 4", children: [], level: 4, slug: "heading-4" },
        ],
        level: 3,
        slug: "heading-32",
      },
    ],
    level: 2,
    slug: "heading-2",
  },
];

Last updated January 4th, 2023