I'm parsing content generated by a wysiwyg
into a table of contents widget in React.
So far I'm looping through the headers and adding them into an array.
How can I get them all into one multi-dimensional array or object (what's the best way) so that it looks more like:
h1-1
h2-1
h3-1
h1-2
h2-2
h3-2
h1-3
h2-3
h3-3
and then I can render it with an ordered list in the UI.
const str = "<h1>h1-1</h1><h2>h2-1</h2><h3>h3-1</h3><p>something</p><h1>h1-2</h1><h2>h2-2</h2><h3>h3-2</h3>";
const patternh1 = /<h1>(.*?)<\/h1>/g;
const patternh2 = /<h2>(.*?)<\/h2>/g;
const patternh3 = /<h3>(.*?)<\/h3>/g;
let h1s = [];
let h2s = [];
let h3s = [];
let matchh1, matchh2, matchh3;
while (matchh1 = patternh1.exec(str))
h1s.push(matchh1[1])
while (matchh2 = patternh2.exec(str))
h2s.push(matchh2[1])
while (matchh3 = patternh3.exec(str))
h3s.push(matchh3[1])
console.log(h1s)
console.log(h2s)
console.log(h3s)
A multidimensional array in MATLAB® is an array with more than two dimensions. In a matrix, the two dimensions are represented by rows and columns. Each element is defined by two subscripts, the row index and the column index.
A multi-dimensional array is an array with more than one level or dimension. For example, a 2D array, or two-dimensional array, is an array of arrays, meaning it is a matrix of rows and columns (think of a table).
I don't know about you, but I hate parsing HTML using regexes. Instead, I think it's a better idea to let the DOM handle this:
const str = `<h1>h1-1</h1>
<h3>h3-1</h3>
<h3>h3-2</h3>
<p>something</p>
<h1>h1-2</h1>
<h2>h2-2</h2>
<h3>h3-2</h3>`;
const wrapper = document.createElement('div');
wrapper.innerHTML = str.trim();
let tree = [];
let leaf = null;
for (const node of wrapper.querySelectorAll("h1, h2, h3, h4, h5, h6")) {
const nodeLevel = parseInt(node.tagName[1]);
const newLeaf = {
level: nodeLevel,
text: node.textContent,
children: [],
parent: leaf
};
while (leaf && newLeaf.level <= leaf.level)
leaf = leaf.parent;
if (!leaf)
tree.push(newLeaf);
else
leaf.children.push(newLeaf);
leaf = newLeaf;
}
console.log(tree);
This answer does not require h3
to follow h2
; h3
can follow h1
if you so please. If you want to turn this into an ordered list, that can also be done:
const str = `<h1>h1-1</h1>
<h3>h3-1</h3>
<h3>h3-2</h3>
<p>something</p>
<h1>h1-2</h1>
<h2>h2-2</h2>
<h3>h3-2</h3>`;
const wrapper = document.createElement('div');
wrapper.innerHTML = str.trim();
let tree = [];
let leaf = null;
for (const node of wrapper.querySelectorAll("h1, h2, h3, h4, h5, h6")) {
const nodeLevel = parseInt(node.tagName[1]);
const newLeaf = {
level: nodeLevel,
text: node.textContent,
children: [],
parent: leaf
};
while (leaf && newLeaf.level <= leaf.level)
leaf = leaf.parent;
if (!leaf)
tree.push(newLeaf);
else
leaf.children.push(newLeaf);
leaf = newLeaf;
}
const ol = document.createElement("ol");
(function makeOl(ol, leaves) {
for (const leaf of leaves) {
const li = document.createElement("li");
li.appendChild(new Text(leaf.text));
if (leaf.children.length > 0) {
const subOl = document.createElement("ol");
makeOl(subOl, leaf.children);
li.appendChild(subOl);
}
ol.appendChild(li);
}
})(ol, tree);
// add it to the DOM
document.body.appendChild(ol);
// or get it as text
const result = ol.outerHTML;
Since the HTML is parsed by the DOM and not by a regex, this solution will not encounter any errors if the h1
tags have attributes, for example.
You can simply gather all h*
and then iterate over them to construct a tree as such:
Using ES6 (I inferred this is ok from your usage of const
and let
)
const str = `
<h1>h1-1</h1>
<h2>h2-1</h2>
<h3>h3-1</h3>
<p>something</p>
<h1>h1-2</h1>
<h2>h2-2</h2>
<h3>h3-2</h3>
`
const patternh = /<h(\d)>(.*?)<\/h(\d)>/g;
let hs = [];
let matchh;
while (matchh = patternh.exec(str))
hs.push({ lev: matchh[1], text: matchh[2] })
console.log(hs)
// constructs a tree with the format [{ value: ..., children: [{ value: ..., children: [...] }, ...] }, ...]
const add = (res, lev, what) => {
if (lev === 0) {
res.push({ value: what, children: [] });
} else {
add(res[res.length - 1].children, lev - 1, what);
}
}
// reduces all hs found into a tree using above method starting with an empty list
const tree = hs.reduce((res, { lev, text }) => {
add(res, lev-1, text);
return res;
}, []);
console.log(tree);
But because your html headers are not in a tree structure themselves (which I guess is your use case) this only works under certain assumptions, e.g. you cannot have a <h3>
unless there's a <h2>
above it and a <h1>
above that. It will also assume a lower-level header will always belong to the latest header of an immediately higher level.
If you want to further use the tree structure for e.g. rendering a representative ordered-list for a TOC, you can do something like:
// function to render a bunch of <li>s
const renderLIs = children => children.map(child => `<li>${renderOL(child)}</li>`).join('');
// function to render an <ol> from a tree node
const renderOL = tree => tree.children.length > 0 ? `<ol>${tree.value}${renderLIs(tree.children)}</ol>` : tree.value;
// use a root node for the TOC
const toc = renderOL({ value: 'TOC', children: tree });
console.log(toc);
Hope it helps.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With