I would like to convert a list of H1 through H6 tags from a markdown file into a Javascript hierarchy for use in a Table of Contents.
Currently the list is generated by AstroJS in this format [{depth: 1, text: 'I am a H1'}, {depth: 2: 'I am a H2}]
.
Caveats
- The markdown is created by end-users.
- This list may have a single root heading (
H1 -> H2 -> H3
), but - It may have multiple root headings (
H2 -> H3, H2 -> H3
) or - It may have a non conventional list of headings (
H3, H2 -> H3
) - It may skip nesting levels (
H1 -> H3 -> H6
)
Looking for a Javascript or Typescript example.
The following three scenarios are based on some Markdown content that is being processed by an AstroJS website.
Single root heading
This standard SEO friendly set of headings has a single H1 followed by other headings
As markdown
# Main heading
## Sub heading 1
### More info
## Sub heading 2
### Even more info
## Sub heading 3 (edge case)
##### Deep nesting
As flat javascript array
headings = [
{ depth: 1, text: 'Main heading' },
{ depth: 2, text: 'Sub heading 1' },
{ depth: 3, text: 'More info' },
{ depth: 2, text: 'Sub heading 2' },
{ depth: 3, text: 'Even more info' },
{ depth: 2, text: 'Sub heading 3 (edge case)' },
{ depth: 6, text: 'Deep nesting' },
]
As javascript hierarchy
list_of_heading_heirachies = [
{ text: 'Main heading', headings: [
{ text: 'Sub heading 1', headings: [
{ text: 'More info', headings: [] },
] },
{ text: 'Sub heading 2', headings: [
{ text: 'Even more info', headings: [] },
] },
{ text: 'Sub heading 3 (edge case)', headings: [
{ text: 'Deep nesting', headings: [] },
] }
]}
]
console.log(list_of_heading_heirachies.length);
// => 1
Multiple root headings
This markdown (common to listicle pages) does not have a single root node like above, instead it has multiple H2s
As markdown
## Why is it done
### Why abc
### Why xyz
## How is it done
### How reason 1
### How reason 2
#### More info
## Conclusion
As flat javascript array
headings = [
{ depth: 2, 'Why is it done' },
{ depth: 3, 'Why abc' },
{ depth: 3, 'Why xyz' },
{ depth: 2, 'How is it done' },
{ depth: 3, 'How reason 1' },
{ depth: 3, 'How reason 2' },
{ depth: 4, 'More info' },
{ depth: 2, 'Conclusion' }
]
As javascript hierarchy
list_of_heading_heirachies = [
{ text: 'Why is it done', headings: [
{ text: 'Why abc', headings: [] },
{ text: 'Why xyz', headings: [] },
] },
{ text: 'How is it done', headings: [
{ text: 'How reason 1', headings: [] },
{ text: 'How reason 2', headings: [
{ text: 'More info', headings: [] },
] },
] },
{ text: 'Conclusion', headings: [] }
]
console.log(list_of_heading_heirachies.length);
// => 3
Non-conventional headings list
This non-conventional headings list happens when there is meta data or breadcrumb data before the general content headings
#### Home -> Blog -> Some Articles
### By Ben Hurr
#### 24th, Sep, 2022
# Some cool Article
## Why abc
### info on why
### more info on why
## How
### How we did it
## Conclusion
As flat javascript array
headings = [
{ depth: 4, text: 'Home -> Blog -> Some Articles' },
{ depth: 3, text: 'By Ben Hurr' },
{ depth: 4, text: '24th, Sep, 2022' },
{ depth: 1, text: 'Some cool Article' },
{ depth: 2, text: 'Why abc' },
{ depth: 3, text: 'info on why' },
{ depth: 3, text: 'more info on why' },
{ depth: 2, text: 'How' },
{ depth: 3, text: 'How we did it' },
{ depth: 2, text: 'Conclusion' },
]
As javascript hierarchy
list_of_heading_heirachies = [
{ text: 'Home -> Blog -> Some Articles', headings: [] },
{ text: 'By Ben Hurr', headings: [
{ text: '24th, Sep, 2022', headings: [] },
] },
{ text: 'Some cool Article', headings: [
{ text: 'Why abc', headings: [
{ text: 'info on why', headings: [] },
{ text: 'more info on why', headings: [] },
] },
{ text: 'How', headings: [
{ text: 'How we did it', headings: [] },
] },
{ text: 'Conclusion', headings: [] },
] },
]
console.log(list_of_heading_heirachies.length);
// => 3
2
Answers
I was able to solve this problem in Typescript.
I am not experienced in this language and while it works fine, someone with more experience may be able to improve this code.
My goal was to take any list of H1..6 and turn it into a Hierarchy and then filter to final Hierarchy to produce a useful Table of Content.
Output Screen Shot
Heading.ts
CreateToc.ts
Example Usage
Sample Data
That seems like a lot of code for the problem.
I would fold the list of nodes into an array of nodes still available to add children to, sorted by depth. We start with an initial node of depth 0. At the end we can simply extract the children of the first node.
If we don’t mind keeping
depth
properties on your node, that’s all it takes. If we really don’t want them, then this can be wrapped in a simple function that recursively removes these properties.Here’s one version:
The real trick is maintaining the current list of open arrays. We maintain it by finding the last node in the list with a lower depth than our current node, adding our current node as a child of that one, cutting the list at that point, and adding our current node to it. Here’s how that array goes as we add node after node from one of your examples:
I think this is significantly simpler than your approach, but I must admit to not having read your approach carefully; it simply seemed too much code for an interesting, but not overly complex, problem.