I have a flat array of pages and I'm trying to create a hierarchical site map from them. What I have so far is very messy so I would like a better way of doing it.
This is the array of pages I have
const pages = [
{
"name": "Page 1",
"url": "/page-1/",
},
{
"name": "Page 2",
"url": "/page-2/",
},
{
"name": "Child 1",
"url": "/page-1/child-1/",
},
{
"name": "Child 2",
"url": "/page-1/child-1/child-2/",
},
{
"name": "Child 3",
"url": "/page-1/child-1/child-2/child-3/",
}
]
and this is the result I want outputting
<ul>
<li>
<a href="/page-1/">Page 1</a>
<ul>
<li>
<a href="/page-1/child-1/">Child 1</a>
<ul>
<li>
<a href="/page-1/child-1/child-2/">Child 2</a>
<ul>
<li>
<a href="/page-1/child-1/child-2/child-3/">Child 3</a>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><a href="/page-2/">Page 2</a></li>
</ul>
This is what I have currently, which works but would like to find a better way to do it
const generateSitemap = function(pages) => {
let sitemap = "";
let organisedPages = [];
const sortPages = function (runs) {
if (pages.length === 0) return;
pages.forEach((page) => {
const title = page.name;
const url = page.url;
// Get homepage and content pages only
let pageObj = {
title: title,
url: url,
children: [],
};
// Handle top level pages first then build up children as you go deeper
if (pageLevel(url) === 1) {
organisedPages.push(pageObj);
pages = pages.filter((page1) => page !== page1);
} else if (runs === 2) {
organisedPages.forEach((oPage, i) => {
// Check to see if url is in segments and matches
let parseUrl = url.substring(1).slice(0, -1);
const urlParts = parseUrl.split("/");
const parentUrl = `/${urlParts.slice(0, -1).join("/")}/`;
if (oPage.url === parentUrl) {
organisedPages[i].children.push(pageObj);
pages = pages.filter(
(page1) => pageObj.url !== page1.systemProperties.url
);
return;
}
});
} else if (runs === 3) {
organisedPages.forEach((oPage, i) => {
// Check to see if url is in segments and matches
let parseUrl = url.substring(1).slice(0, -1);
const urlParts = parseUrl.split("/");
const parentUrl = urlParts.slice(0, -1);
const parentUrlComp = `/${parentUrl.join("/")}/`;
const parentUrl2 = parentUrl.slice(0, -1);
const parentUrl2Comp = `/${parentUrl2.join("/")}/`;
if (oPage.url === parentUrl2Comp) {
organisedPages[i].children.forEach((child, j) => {
if (child.url === parentUrlComp) {
organisedPages[i].children[j].children.push(pageObj);
pages = pages.filter(
(page1) => pageObj.url !== page1.systemProperties.url
);
return;
}
});
}
});
} else if (runs === 4) {
organisedPages.forEach((oPage, i) => {
// Check to see if url is in segments and matches
let parseUrl = url.substring(1).slice(0, -1);
const urlParts = parseUrl.split("/");
const parentUrl = urlParts.slice(0, -1);
const parentUrlComp = `/${parentUrl.join("/")}/`;
const parentUrl2 = parentUrl.slice(0, -1);
const parentUrl2Comp = `/${parentUrl2.join("/")}/`;
const parentUrl3 = parentUrl2.slice(0, -1);
const parentUrl3Comp = `/${parentUrl3.join("/")}/`;
if (oPage.url === parentUrl3Comp) {
organisedPages[i].children.forEach((child, j) => {
if (child.url === parentUrl2Comp) {
organisedPages[i].children[j].children.forEach((child1, k) => {
if (child1.url === parentUrlComp) {
organisedPages[i].children[j].children[k].children.push(
pageObj
);
pages = pages.filter(
(page1) => pageObj.url !== page1.systemProperties.url
);
return;
}
});
}
});
}
});
}
});
runs ;
if (runs < 5) {
sortPages(runs);
}
};
sortPages(1);
/**
* Check if page is a parent
*
* @param {string} url page url
* @returns {number} length of segments
*/
function pageLevel(url) {
// Remove first and last forward slash that is provided
let parseUrl = url.substring(1).slice(0, -1);
// Split parsed url by forward slash
const urlParts = parseUrl.split("/");
// Check segment length
return urlParts.length;
}
/**
* Loop through organised pages and set listing.
*/
organisedPages.forEach((page) => {
sitemap = `<li>`;
sitemap = `<a href="${page.url}">${page.title}</a>`;
// Check if we need children loop for each parent page
if (page.children.length) {
sitemap = `<ul>`;
page.children.forEach((page) => {
sitemap = `<li>`;
sitemap = `<a href="${page.url}">${page.title}</a>`;
// Check if we need children loop for each sub-child page
if (page.children.length) {
sitemap = `<ul>`;
page.children.forEach((page) => {
sitemap = `<li>`;
sitemap = `<a href="${page.url}">${page.title}</a>`;
if (page.children.length) {
sitemap = `<ul>`;
page.children.forEach((page) => {
sitemap = `<li>`;
sitemap = `<a href="${page.url}">${page.title}</a>`;
sitemap = `</li>`;
});
sitemap = `</ul>`;
}
sitemap = `</li>`;
});
sitemap = `</ul>`;
}
sitemap = `</li>`;
});
sitemap = `</ul>`;
}
sitemap = `</li>`;
});
return sitemap;
};
generateSitemap(pages)
CodePudding user response:
I would choose to break the logical nesting of objects from the HTML formatting, as I think it makes for simpler functions all around. To comfortably do the nesting, I will also add a helper function to get the parent id from a url, so that, for instance,
getParentId ("/page-1/") //=> ""
getParentId ("page-1/child-1/") //=> "page-1"
getParentId ("page1/child-1/child-2/") //=> "page1/child-1"
// ... etc
We could easily inline this function in nest
, but I think it's cleaner as a helper. There is a little bit of odd complexity with this, because there is an extra slash somewhere, beginning or end. We choose to slice off the final /
when searching through our list to find children.
The code looks like this:
const getParentId = (url) => url .slice (0, url .slice (0, -1) .lastIndexOf ('/'))
const nest = (pages, parentId = "", id = parentId .slice (0, -1)) => pages
.filter (({url}) => getParentId (url) == id)
.map (({url, ...rest}) => ({...rest, url, children: nest (pages, url)}))
const format = (nodes) => `<ul>${nodes .map (({name, url, children}) =>
`<li><a href="${url}">${name}</a>${children .length ? format (children) : ''}</li>`
).join('')}</ul>`
const pages2html = (pages) => format (nest (pages))
const pages = [{name: "Page 1", url: "/page-1/"}, {name: "Page 2", url: "/page-2/"}, {name: "Child 1", url: "/page-1/child-1/"}, {name: "Child 2", url: "/page-1/child-1/child-2/"}, {name: "Child 3", url: "/page-1/child-1/child-2/child-3/"}]
console .log (pages2html (pages))
.as-console-wrapper {max-height: 100% !important; top: 0}
Here nest
turns your nodes into a format like this:
[
{
name: "Page 1",
url: "/page-1/",
children: [
{
name: "Child 1",
url: "/page-1/child-1/",
children: [
{
name: "Child 2",
url: "/page-1/child-1/child-2/",
children: [
{
name: "Child 3",
url: "/page-1/child-1/child-2/child-3/",
children: []
}
]
}
]
}
]
},
{
name: "Page 2",
url: "/page-2/",
children: []
}
]
and then format
turns that into your HTML. We wrap them together in pages2html
to have a single function to call, but the work is done in those two at-least-possibly reusable functions.
Note that we don't try to preserve the white-space in your requested format. We could do so, at the expense of making format
quite a bit uglier. Here's a quick attempt, which I think is correct. But I wouldn't swear to it:
const format = (nodes, depth = 0) => `${depth > 0 ? '\n' : ''
}${' '.repeat (2 * depth) }<ul>${nodes .map (({name, url, children}) => `
${' ' .repeat (2 * depth 1) }<li>
${' ' .repeat (2 * depth 2) }<a href="${url}">${name}</a>${
children .length ? `` format (children, depth 1) : ''
}
${' ' .repeat (2 * depth 1) }</li>`
).join('')}
${' '.repeat (2 * depth)}</ul>`
We simply use a depth
parameter to note the level of nesting, and use that to figure out how many spaces to use at the beginning of lines.
In general, I find this style, working as a sequence of transformations, much simpler to work with. Yes, we could do all this in a single function. It might even have fewer lines of code than my four separate functions. But each of these is simpler to understand, and simpler to change when the need arises.
And recursion will be more flexible and much simpler than your multi-level branching, as you can see from the fairly simple recursive code in next
.