I am writing a function that queries all heading elements that are direct children of a div and stores them as an item in an array (data
) where a heading with a lower tag name is an item
of the heading above it. In other words, creating a tree structure for the headings. Each heading is in the data
array is an object with the following properties:
text
: a string with the inner text of the heading.
items
: an array with possible subheading items.
number
: a string that represents the position of the heading in the document.
id
: a string that is composed of the text of the element and it’s parents texts, separated by two underscores “__”.
Getting the names and numbers right, I had to store the last ones for each heading level in an array, to access them while traversing through the items. This is what I came up with:
const contentContainer = document.getElementById('content');
// Query headings that are direct children of the content container.
const headings = contentContainer.querySelectorAll(":scope > h1,:scope > h2,:scope > h3,:scope > h4,:scope > h5,:scope > h6");
// String helper function
function kebabCase(string) {
return (string
.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/[s_]+/g, '-').toLowerCase()
)
}
// Contains the list of headings and their items.
const data = [];
// Array that stores the last depth of each of the last headings at each depth.
const indexHelper = new Array(6).fill(-1);
// Array that stores the last name of each of the last headings at each depth.
const nameHelper = new Array(6).fill('');
// Generates a datalist of headings and sets an id for each heading in the DOM.
function processTag(depth, element) {
const listItem = {
text: element.innerText,
items: []
};
const name = kebabCase(element.innerText);
// Increment the indexHelper at depth position and set the current name.
indexHelper[depth]++;
nameHelper[depth] = name;
// Reset rest of array that is bigger than current depth.
indexHelper.fill(-1, depth + 1);
// Push the element to the right place in the array.
let dataPushString = 'data';
for (let i = 0; i < depth; i++) {
dataPushString += "[indexHelper[".concat(i, "]]?.items?");
i < depth - 1 && (dataPushString += '.');
}
dataPushString += '.push(listItem)';
eval(dataPushString);
// Generate an id based on the names of the element and its parents.
let id = '';
for (let i = 0; i < depth; i++) {
id += nameHelper[i] + "__";
}
let number = '';
for (let i = 0; i < depth; i++) {
number += "(indexHelper[".concat(i + 1, "] +1) + "." ");
i < depth - 1 && (number += ' + ');
}
listItem.id = id + name;
listItem.number = eval(number) && eval(number);
}
function processData() {
for (let i = 0; i < headings.length; i++) {
const element = headings[i];
const depth = Number(element.tagName.substring(1) - 1);
processTag(depth, element);
}
}
processData();
const pre = document.getElementById('pre');
pre.innerHTML = JSON.stringify(data, null, 2);
<pre id="pre"></pre>
<div id="content">
<h1>Title</h1>
<h3>This heading is problematic</h3>
<h2>H2</h2>
<h2>Another h2</h2>
<h3>Depth h3 </h3>
<h4>Fourth level</h4>
<h3>Back to three</h3>
<h2>Back to two</h2>
</div>
As you can see in the example above, the malformatted <h3>This heading is problematic</h3>
not added to data
. The reason being, I am looping over the item and try to add it to it’s parent that does not exist. This is done by creating concatenating a string to the right “depth” using eval(), which also is a bad idea.
My question is twofold: how can I make sure the problematic heading is added to data
even though it’s (direct) parent heading does not exist? And how do I get around not using eval()?
The result I am hoping to get is:
[
{
"text": "Title",
"items": [
{
"text": "",
"items": [
{
"text": "This heading is problematic",
"items": [],
"id": "title____this-heading-is-problematic",
"number": "2.1.1"
},
],
"id": "title__",
"number": "1."
},
{
"text": "H2",
"items": [],
"id": "title__h2",
"number": "2."
},
{
"text": "Another h2",
"items": [
//...etc...