Skip to content

Instantly share code, notes, and snippets.

@junhan-z
Last active February 7, 2021 02:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save junhan-z/baa3ea5bd3faf77b70927151c1387278 to your computer and use it in GitHub Desktop.
Save junhan-z/baa3ea5bd3faf77b70927151c1387278 to your computer and use it in GitHub Desktop.
Gatsby Table of Content implementation
import React from "react"
import kebabCase from "lodash/kebabCase"
class HeaderNode {
constructor(value, depth) {
this.value = value
this.depth = depth
this.children = []
}
render() {
if (this.children.length === 0) {
if (this.value === undefined) {
// TODO: impossible, should raise issue
return (<></>)
}
return (<li>{this.value}</li>)
}
var childrenRender = (
<ul>
{this.children.map(child => (child.render()))}
</ul>
)
if (this.value === undefined) {
// this node is a dummy level, wrap another level
return <li>{childrenRender}</li>
}
return (
<li>
<p>{this.value}</p>
{childrenRender}
</li>
)
}
}
function _createLink(slug, heading) {
let anchor = kebabCase(heading.value)
let to = slug + "#" + anchor
return (
<Link to={to} key={anchor}>
{heading.value}
</Link>)
}
function _createNodes(rootDepth, slug, headings) {
var stack = []
let roots = []
for (var i = 0; i < headings.length; i++) {
let heading = headings[i]
let value = _createLink(slug, heading)
if (heading.depth === rootDepth) {
let root = new HeaderNode(value, heading.depth)
roots.push(root)
stack = [root] // set stack with a fresh start
continue
}
// this heading is not a root (depth !== 1)
if (stack.length === 0) {
// push a dummy root node to start
let root = new HeaderNode(undefined, rootDepth)
roots.push(root)
stack = [root]
}
// push dummy node until its parent level
var depth = heading.depth
while (depth - 1 > stack[stack.length - 1].depth) {
var parent = stack[stack.length - 1]
var child = new HeaderNode(undefined, parent.depth + 1)
parent.children.push(child)
stack.push(child)
}
// find the parent
while (heading.depth - 1 !== stack[stack.length - 1].depth) {
stack.pop()
}
var node = new HeaderNode(value, heading.depth)
stack[stack.length - 1].children.push(node)
stack.push(node)
}
return roots
}
function createToc(slug, headings) {
var rootDepth = undefined
for (var i = 0; i < headings.length; i++) {
var depth = headings[i].depth
if (rootDepth === undefined || depth < rootDepth) {
rootDepth = depth
}
}
let nodes = _createNodes(rootDepth, slug, headings)
return (
<ul>
{nodes.map(node => node.render())}
</ul>
)
}
// Feed in an array of table of contents that contains depth and value for each header
// this component renders a table of content the same as the raw HTML that returned from
// quering the `TableOfContents` fields in GraphQL in Gatsby.
// With the flexibilty provided, you can customize the heading element more easily
export default function TableOfContents({ post }) {
let headings = post.headings.filter(heading => heading.depth < 3)
if (headings.length === 0) {
return (
<></>
)
}
return (
<div className={`${styles.TableOfContents}`}>
<div><b>Table of Contents</b></div>
<section>
{createToc(post.fields.slug, headings)}
</section>
</div>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment