Given the following input:
<dl>
<dt>
<h3>Title A</h3>
<dl>
<dt>
<h3>Title A- A</h3>
<dl>
<dt><a href="#">Item</a></dt>
<dt><a href="#">Item</a></dt>
</dl>
</dt>
<dt><a href="#">Item</a></dt>
<dt><a href="#">Item</a></dt>
<dt><a href="#">Item</a></dt>
<dt><a href="#">Item</a></dt>
<dt>
<h3>Title B- A</h3>
<dl>
<dt><a href="#">Item</a></dt>
<dt><a href="#">Item</a></dt>
</dl>
</dt>
<dt><a href="#">Item</a></dt>
</dl>
</dt>
</dl>
I want to build an JSON object based on the above input:
{
"title": "Title A",
"children": [
{
"title": "Title A- A",
"children": [
{"title": "Item"},
{"title": "Item"}
]
},
{"title": "Item"},
{"title": "Item"},
{"title": "Item"},
{"title": "Item"},
{
"title": "Title B- A",
"children": [
{"title": "Item"},
{"title": "Item"}
]
},
{"title": "Item"}
]
}
Here’s what I have tried so far:
function buildTree(node) {
if (!node) return [];
const h3 = node.querySelector('h3') || node.querySelector('a');
let result = {
title: h3.innerText,
children: []
};
const array = [...node.querySelectorAll('dl')];
if (array) {
result.children = array.map(el => buildTree(el.querySelector('dt')));
}
return result;
}
The result I’m getting is different from what I expect, Here’s the result I am getting:
{
"title": "Title A",
"children": [
{
"title": "Title A",
"children": [
{
"title": "Title A- A",
"children": [
{
"title": "Item A- A 1",
"children": []
}
]
},
{
"title": "Item A- A 1",
"children": []
},
{
"title": "Title B- A 1",
"children": []
}
]
},
{
"title": "Title A- A",
"children": [
{
"title": "Item A- A 1",
"children": []
}
]
},
{
"title": "Item A- A 1",
"children": []
},
{
"title": "Title B- A 1",
"children": []
}
]
}
seems that some data are not there, Any idea what I might be missing?
Advertisement
Answer
fix html
First I would remark that you are misusing dl. From the MDN docs –
The HTML
<dl>element represents a description list. The element encloses a list of groups of terms (specified using the<dt>element) and descriptions (provided by<dd>elements) …
Here’s what the correct use of dl, dt, and dd would look like –
<dl>
<dt>Title 1</dt>
<dd>
<dl>
<dt>Title 1.1</dt>
<dd><a href="#">Item 1.1.1</a></dd>
<dd><a href="#">Item 1.1.2</a></dd>
</dl>
</dd>
<dd><a href="#">Item 1.2</a></dd>
<dd><a href="#">Item 1.3</a></dd>
<dd><a href="#">Item 1.4</a></dd>
<dd><a href="#">Item 1.5</a></dd>
<dd>
<dl>
<dt>Title 1.6</dt>
<dd><a href="#">Item 1.6.1</a></dd>
<dd><a href="#">Item 1.6.2</a></dd>
</dl>
</dd>
<dd><a href="#">Item 1.7</a></dd>
</dl>
Notice it matches the expected shape of your output –
{
"title": "Title 1",
"children": [
{
"title": "Title 1.1",
"children": [
{"title": "Item 1.1.1"},
{"title": "Item 1.1.2"}
]
},
{"title": "Item 1.2"},
{"title": "Item 1.3"},
{"title": "Item 1.4"},
{"title": "Item 1.5"},
{
"title": "Title 1.6",
"children": [
{"title": "Item 1.6.1"},
{"title": "Item 1.6.2"}
]
},
{"title": "Item 1.7"}
]
}
fromHtml
If you are not willing (or able) to change the input html as described above, please see Scott’s wonderful answer. To write a program for the proposed html, I would break it into two parts. First we write fromHtml with a simple recursive form –
function fromHtml (e)
{ switch (e?.tagName)
{ case "DL":
return Array.from(e.childNodes, fromHtml).flat()
case "DD":
return [ Array.from(e.childNodes, fromHtml).flat() ]
case "DT":
case "A":
return e.textContent
default:
return []
}
}
fromHtml(document.querySelector('dl'))
Which gives us this intermediate format –
[
"Title 1",
[
"Title 1.1",
[ "Item 1.1.1" ],
[ "Item 1.1.2" ]
],
[ "Item 1.2" ],
[ "Item 1.3" ],
[ "Item 1.4" ],
[ "Item 1.5" ],
[
"Title 1.6",
[ "Item 1.6.1" ],
[ "Item 1.6.2" ]
],
[ "Item 1.7" ]
]
applyLabels
Following that, I would write a separate applyLabels function which adds the title and children labels you require –
const applyLabels = ([ title, ...children ]) =>
children.length
? { title, children: children.map(applyLabels) }
: { title }
const result =
applyLabels(fromHtml(document.querySelector('dl')))
{
"title": "Title 1",
"children": [
{
"title": "Title 1.1",
"children": [
{"title": "Item 1.1.1"},
{"title": "Item 1.1.2"}
]
},
{"title": "Item 1.2"},
{"title": "Item 1.3"},
{"title": "Item 1.4"},
{"title": "Item 1.5"},
{
"title": "Title 1.6",
"children": [
{"title": "Item 1.6.1"},
{"title": "Item 1.6.2"}
]
},
{"title": "Item 1.7"}
]
}
I might suggest one final change, which guarantees all nodes in the output have a uniform shape, { title, children }. It’s a change worth noting because in this case applyLabels is easier to write and it behaves better –
const applyLabels = ([ title, ...children ]) =>
({ title, children: children.map(applyLabels) })
Yes, this means that deepest descendants will have an empty children: [] property, but it makes consuming the data much easier as we don’t have to null-check certain properties.
demo
Expand the snippet below to verify the results of fromHtml and applyLabels in your own browser –
function fromHtml (e)
{ switch (e?.tagName)
{ case "DL":
return Array.from(e.childNodes, fromHtml).flat()
case "DD":
return [ Array.from(e.childNodes, fromHtml).flat() ]
case "DT":
case "A":
return e.textContent
default:
return []
}
}
const applyLabels = ([ title, ...children ]) =>
children.length
? { title, children: children.map(applyLabels) }
: { title }
const result =
applyLabels(fromHtml(document.querySelector('dl')))
console.log(result)<dl>
<dt>Title 1</dt>
<dd>
<dl>
<dt>Title 1.1</dt>
<dd><a href="#">Item 1.1.1</a></dd>
<dd><a href="#">Item 1.1.2</a></dd>
</dl>
</dd>
<dd><a href="#">Item 1.2</a></dd>
<dd><a href="#">Item 1.3</a></dd>
<dd><a href="#">Item 1.4</a></dd>
<dd><a href="#">Item 1.5</a></dd>
<dd>
<dl>
<dt>Title 1.6</dt>
<dd><a href="#">Item 1.6.1</a></dd>
<dd><a href="#">Item 1.6.2</a></dd>
</dl>
</dd>
<dd><a href="#">Item 1.7</a></dd>
</dl>remarks
I’ve written hundreds of answers on the topic of recursion and data transformation and yet this is the first time I think I’ve used .flat in an essential way. I thought I had a use case in this Q&A but Scott’s comment took it from me! This answer differs because domNode.childNodes is not a true array and so Array.prototype.flatMap cannot be used. Thanks for the interesting problem.