Javascript 如何从节点列表递归构造JSON层次结构?

Javascript 如何从节点列表递归构造JSON层次结构?,javascript,recursion,Javascript,Recursion,考虑到以下输入: <dl> <dt> <h3>Title A</h3> <dl> <dt> <h3>Title A- A</h3> <dl> <dt><a href="#">Item</a></dt> <dt>

考虑到以下输入:

<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>
以下是我迄今为止所尝试的:

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;
}
我得到的结果与我预期的不同,以下是我得到的结果:

{
  "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": []
    }
  ]
}

似乎有些数据不存在,你知道我可能遗漏了什么吗?

我认为一个重要的部分是querySelector和querySelectorAll是递归的。可能是你混淆了dl和dt,因为dl中有多个dt?以下内容对你有用吗

function buildTree(node) {
    if (!node) return [];
    const h3 = node.querySelector(':scope > h3') || node.querySelector(':scope > a');
    let result = {
        title: h3.innerText,
        children: []
    };
    const array = [...node.querySelectorAll(':scope > dl > dt')];
    result.children = array.map(el => buildTree(el));
    return result;
}

您必须首先传递一个dt节点才能使其工作。()

你最好在递归之前做出决定:

函数构建树(节点){
const result={};
for(节点子节点的常数){
开关(el.nodeName){
案例“H3”:
案例“A”:
result.title=el.innerText;
结果:儿童=[];
打破
案例“DL”:
result.children=buildTree(el);
打破
案例“DT”:
结果.children.push(buildTree(el));
打破
违约:
warn(`Unknown node type'${el.nodeName}`,el);
}
}
返回结果;
}

在这个例子中,我可以看到您试图解析几乎相同的
DT
s和
DL
s。

这是一个明确的相互递归案例。如果我们区分如何处理
DL
和如何处理
DT
,这是一个简单的过程。(正如其他人所指出的,没有任何
DD
,这是一种奇怪的结构。)

我在初始的
DL
中添加了一个
id
,以便于掌握。但是,无论您选择如何获取这个根元素,您都应该能够将它传递给
handleDl
,以恢复您的结构

const handleDl=(dl)=>
[…dl.儿童]
.filter(({nodeName})=>nodeName=='DT')
.地图(handleDt)
常数handleDt=(dt)=>{
const kids=[…dt.children]
const h3=kids.find(({nodeName})=>nodeName=='h3')
const dl=kids.find(({nodeName})=>nodeName=='dl')
返回h3
?{title:h3.textContent,children:handleDl(dl)}
:{title:dt.textContent}
}
const node=document.getElementById('foo')
console.log(handleDl(节点))
。作为控制台包装{最大高度:70%!重要;顶部:30%}

标题A-A-B-A
修复html

首先,我要指出,您误用了
dl
。从-

HTML
元素表示一个描述列表。元素包含一组术语(使用
元素指定)和描述(由
元素提供)的列表

下面是正确使用
dl
dt
dd
的情况-


标题1
标题1.1
标题1.6
请注意,它与输出的预期形状相匹配-

{
“标题”:“标题1”,
“儿童”:[
{
“标题”:“标题1.1”,
“儿童”:[
{“标题”:“第1.1.1项”},
{“标题”:“第1.1.2项”}
]
},
{“标题”:“第1.2项”},
{“标题”:“第1.3项”},
{“标题”:“第1.4项”},
{“标题”:“第1.5项”},
{
“标题”:“标题1.6”,
“儿童”:[
{“标题”:“第1.6.1项”},
{“标题”:“第1.6.2项”}
]
},
{“标题”:“项目1.7”}
]
}

fromHtml

如果您不愿意(或不能)如上所述更改输入html,请参阅Scott的精彩答案。要为提议的html编写一个程序,我将把它分成两部分。首先,我们用一个简单的递归形式从HTML编写
-

函数fromHtml(e) {开关(e?.tagName) {案例“DL”: 返回Array.from(e.childNodes,fromHtml).flat() 案例“DD”: return[Array.from(e.childNodes,fromHtml).flat()] 案例“DT”: 案例“A”: 返回e.textContent 违约: 返回[] } } fromHtml(document.querySelector('dl'))
这给了我们这个中间格式-

[
“标题1”,
[
“标题1.1”,
[“项目1.1.1”],
[“第1.1.2项”]
],
[“项目1.2”],
[“项目1.3”],
[“项目1.4”],
[“项目1.5”],
[
“标题1.6”,
[“第1.6.1项],
[“第1.6.2项”]
],
[“项目1.7”]
]

applyLabels

接下来,我将编写一个单独的
applyLabels
函数,该函数添加所需的
title
子标签-

const applyLabels=([title,…children])=>
儿童身高
? {标题,子对象:children.map(applyLabels)}
:{title}
常数结果=
ApplyLabel(来自HTML(document.querySelector('dl'))
{
“标题”:“标题1”,
“儿童”:[
{
“标题”:“标题1.1”,
“儿童”:[
{“标题”:“第1.1.1项”},
{“标题”:“第1.1.2项”}
]
},
{“标题”:“第1.2项”},
{“标题”:“第1.3项”},
{“标题”:“第1.4项”},
{“标题”:“第1.5项”},
{
“标题”:“标题1.6”,
“儿童”:[
{“标题”:“第1.6.1项”},
{“标题”:“第1.6.2项”}
]
},
{“标题”:“项目1.7”}
]
}
我可能会建议最后一个更改,它保证输出中的所有节点具有统一的形状,
{title,children}
。这是一个值得注意的变化,因为在本例中,
applyLabels
更易于编写,而且性能更好-

const applyLabels=([title,…children])=>
({标题,子项:children.map(applyLabels)})
是的,这意味着最深的子代将有一个空的
子代:[]
属性,但它使使用数据变得更容易,因为我们不必对某些属性进行空检查


演示

展开下面的代码段,在您自己的浏览器中验证HTML和applyLabels的结果-<
function buildTree(node) {
    if (!node) return [];
    const h3 = node.querySelector(':scope > h3') || node.querySelector(':scope > a');
    let result = {
        title: h3.innerText,
        children: []
    };
    const array = [...node.querySelectorAll(':scope > dl > dt')];
    result.children = array.map(el => buildTree(el));
    return result;
}