Basic markdown it setup
If you want to skip this process and run the demo on your personal computer you can clone the demo repository on Github
On your javascript application, we want to install some dependencies first.
yarn add --dev markdown-it markdown-it-anchor
Now let's convert some simple markdown to Html:
// demo.js const MarkdownIt = require('markdown-it'); const md = new MarkdownIt().use(require('markdown-it-anchor')); const markdownText = ` # First header Some content ## Second-level header More content ### Third level header ## Another second-level header `; const html = md.render(markdownText); console.log({ html });
Plain table of content
The first approach will be to create a plain TOC, that returns a one level array with the title, the anchor, and the associated level. Using this type of TOC could give you a really clear component, avoiding the extra complexity of recursion.
function createPlainToc(domElement) { const headers = Array.from( domElement.querySelectorAll('h1, h2, h3, h4, h5, h6, h7') ); // This will create a plain array with the level, the anchor and the header title return headers.map((header) => ({ level: parseInt(header.tagName[1], 10), title: header.textContent, anchor: '#' + header.getAttribute('id'), })); }
Using the above markdown text, it will return us the following output:
[ { level: 1, title: 'First header', anchor: '#first-header' }, { level: 2, title: 'Second level header', anchor: '#second-level-header' }, { level: 3, title: 'Third level header', anchor: '#third-level-header' }, { level: 2, title: 'Another second level header', anchor: '#another-second-level-header', }, ];
Alternatively you can test the above function with this page itself. You can copy the function and run the following code on the browser's console
console.log( JSON.stringify( createPlainToc(window.document.querySelector('.articleBody')) ) );
Tree table of content
Most of the time we will be interested in creating a tree representation of the data. In this way, we can create a recursive component that runs through the tree. We can reuse our previous function and build the data using a stack and tacking advantage of object references in Javascript. We must perform two validations:
- We force all root nodes to have the same level, but not a minimum one.
- We force all children nodes to have the parent's level + 1.
function createTreeToc(domElement, throwOnNotTree = true) { const treeItems = createPlainToc(domElement).map((item) => ({ ...item, children: [], })); const tree = []; const stack = []; treeItems.forEach((item) => { let parentItem = null; do { parentItem = stack.pop(); } while (parentItem && parentItem.level >= item.level); if (!parentItem) { // All nodes in tree[] must have the same level. const prevTreeItem = tree.pop(); if (prevTreeItem) { if (throwOnNotTree && item.level !== prevTreeItem.level) { throw new Error( `Root titles dont have same level on: "${item.title}"` ); } tree.push(prevTreeItem); } tree.push(item); stack.push(item); } else { if (throwOnNotTree && parentItem.level !== item.level - 1) { // All nodes in tree[] must have the same level. throw new Error( `The title "${item.title}" is not a direct children of its parent` ); } stack.push(parentItem); stack.push(item); parentItem.children.push(item); } }); return tree; }
Using the previous markdown we will have a well-defined tree for each top-level header and their associated children.
[ { level: 1, title: 'First header', anchor: '#first-header', children: [ { level: 2, title: 'Second level header', anchor: '#second-level-header', children: [ { level: 3, title: 'Third level header', anchor: '#third-level-header', children: [], }, ], }, { level: 2, title: 'Another second level header', anchor: '#another-second-level-header', children: [], }, ], }, ];
Of course opening the dev tools you can paste the previous functions, an test it with this page!
console.log( JSON.stringify(createTreeToc(window.document.querySelector('.articleBody'))) );
Create the tables of content on the server-side with jsdom
If you want to create the TOC on your node server or at the moment of compiling your markdown files, we can reuse our functions by installing jsdom.
yarn add --dev jsdom
As the Html will be the hole markdown file we can create the table of content for the whole window.document
.
// Asuming that html is the rendered markdown with html and nothing else. const domElement = new jsdom.JSDOM(html).window.document; const toc = createTreeToc(domElement); console.log(JSON.stringify(toc));
Using this last approach its how I set up a simple demo on Github.
Conclusion
Using jsdom
to parse the whole HTML file to get the table of content might look like an overhead. However, doing this stuff at your javascript compile stage will get you the fastest loading times for your web because it could be server-side rendered. On the other hand, if you parse your TOC in the browser you could access all the DOM elements on the web and not only the markdown ones.
Thank you very much for your time. Hope to see you again 🚀