将数组添加到多维数组或对象中

19

我正在将由 wysiwyg 生成的内容解析为React中的目录小部件。

到目前为止,我正在循环遍历标题并将它们添加到一个数组中。

我如何将它们全部放入一个多维数组或对象中(最佳方法是什么),以便它看起来更像:

h1-1
    h2-1
        h3-1

h1-2
    h2-2
        h3-2

h1-3
    h2-3
        h3-3

然后我可以在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)

5个回答

12

我不知道你是否有同感,但我讨厌使用正则表达式来解析HTML。相反,我认为让DOM处理这个任务是更好的选择:

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);

这个答案不需要让 h3 跟在h2后面;如果您愿意,h3 也可以跟在h1后面。如果您想将其转换为有序列表,也可以这样做:

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;

由于HTML是由DOM解析而不是通过正则表达式解析,因此如果

标签具有属性,此解决方案将不会遇到任何错误。


7
你可以简单地收集所有的h*,然后遍历它们以构建一个树形结构,如下所示:
使用ES6(我从你使用的constlet中推断出这是可行的)
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);

但是,因为您的html标题本身不是树形结构(我猜这是您的用例),所以只有在特定的假设下才能正常工作,例如,除非上面有一个

和一个

,否则您不能有一个

。它还将假定较低级别的标题始终属于立即更高级别的最新标题。
如果您想进一步使用树形结构,例如呈现有序列表以用于目录,请执行以下操作:
// 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);

希望这能有所帮助。


太棒了。你能向我展示如何将树解析成有序列表吗? - totalnoob
太好了!大部分情况下都能正常工作。只有一个使用情况,即存在多个h2标签的情况。假设在h3标签之后还有两个h2标签,你会如何处理它们在渲染时? - totalnoob
我不确定你的意思 - 看一下http://jsfiddle.net/d5jemfyn/2 我添加了更多的h2和更复杂的结构 - 结果看起来还不错,也许我漏掉了什么。 - Ovidiu Dolha
不错,这正是我想到的简洁方法(使用reduce)!我会点赞的,但我强烈建议您使用更健壮/安全的 var dom = new DOMParser().parseFromString(str, 'text/html'); var hs = Array.from(dom.body.querySelectorAll('h1,h2,h3,h4,h5,h6')) 或等效的方式。 - ninjagecko

5
你想要做的是创建一个嵌套列表,从文档标题中按层次结构进行排列。这被称为(变体的)文档大纲。
使用DOM和DOMParser API在浏览器上实现此功能的简单方法如下(将其放在HTML页面中并编码为ES5以便于测试):
<!DOCTYPE html>
<html>
<head>
<title>Document outline</title>
</head>
<body>
<div id="outline"></div>
<script>

// test string wrapped in a document (and body) element
var str = "<html><body><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></body></html>";

// util for traversing a DOM and emit SAX startElement events
function emitSAXLikeEvents(node, handler) {
    handler.startElement(node)
    for (var i = 0; i < node.children.length; i++)
        emitSAXLikeEvents(node.children.item(i), handler)
    handler.endElement(node)
}

var outline = document.getElementById('outline')
var rank = 0
var context = outline
emitSAXLikeEvents(
    (new DOMParser()).parseFromString(str, "text/html").body,
    {
        startElement: function(node) {
            if (/h[1-6]/.test(node.localName)) {
                var newRank = +node.localName.substr(1, 1)

                // set context li node to append
                while (newRank <= rank--)
                    context = context.parentNode.parentNode

                rank = newRank

                // create (if 1st li) or
                // get (if 2nd or subsequent li) ol element
                var ol
                if (context.children.length > 0)
                    ol = context.children[0]
                else {
                    ol = document.createElement('ol')
                    context.appendChild(ol)
                }

                // create and append li with text from
                // heading element
                var li = document.createElement('li')
                li.appendChild(
                  document.createTextNode(node.innerText))
                ol.appendChild(li)

                context = li
            }
        },
        endElement: function(node) {}
    })
</script>
</body>
</html>

我首先将你的片段解析为一个Document,然后遍历它以创建类似SAX的startElement()调用。在startElement()函数中,检查标题元素的级别是否高于最近创建的列表项(如果有)。然后在正确的层次结构级别上附加新的列表项,并可能创建一个ol元素作为其容器。请注意,此算法不能处理从层次结构中的h1跳转到h3之类的情况,但可以轻松地进行调整。
如果要在node.js上创建大纲/目录,则可以使代码在服务器端运行,但需要一个不错的HTML解析库(可以说是node.js的DOMParser polyfill)。还有https://github.com/h5o/h5o-jshttps://github.com/hoyois/html5outliner包可用于创建大纲,尽管我没有测试过这些包。这些包据说也可以处理一些特殊情况,例如在iframequote元素中的标题元素,通常不希望将其包含在文档大纲中。
创建HTML5大纲的主题具有悠久的历史;例如,参见http://html5doctor.com/computer-says-no-to-html5-document-outline/。HTML4的做法是不使用任何分段根(在HTML5术语中)包装元素进行分段,并将标题和内容放置在相同的层次结构级别上,这被称为“平面地球标记”。SGML具有用于处理H1H2等级元素的RANK特性,并且可以通过简单情况下的类似HTML4的“平面地球标记”(例如仅允许一个section或另一个单一元素作为分段根)来推断省略的section元素,从而自动创建大纲。

使用DOMParser接口似乎是唯一正确的方法(今天我学到了新东西!)。 其他任何方法都可能在包含<h1>、奇怪属性等脚本标记上失败(引入可能的安全漏洞...尽管这仍然是问题所在)。 - ninjagecko

2
我将使用一个正则表达式来获取 <hx></hx> 标签中的内容,并使用 Array.reduce 方法按 x 进行排序。
以下是基础内容,但还没有结束:

// The string you need to parse
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>";

// The regex that will cut down the <hx>something</hx>
const regex = /<h[0-9]{1}>(.*?)<\/h[0-9]{1}>/g;

// We get the matches now
const matches = str.match(regex);

// We match the hx togethers as requested
const matchesSorted = Object.values(matches.reduce((tmp, x) => {
  // We get the number behind hx ---> the x
  const hNumber = x[2];

  // If the container do not exist, create it
  if (!tmp[hNumber]) {
    tmp[hNumber] = [];
  }

  // Push the new parsed content into the array
  // 4 is to start after <hx>
  // length - 9 is to get all except <hx></hx>
  tmp[hNumber].push(x.substr(4, x.length - 9));

  return tmp;
}, {}));

console.log(matchesSorted);


由于您正在解析HTML内容,我想提醒您注意特殊情况,例如\n空格的存在。例如,请查看以下不起作用的代码片段:

// The string you need to parse
const str = "\
 <h1>h1-1\n\
 </h1>\
 <h2>  h2-1</h2>\
 <h3>h3-1</h3>\
 <p>something</p>\
 <h1>h1-2  </h1>\
 <h2>h2-2 \n\
 </h2>\
 <h3>h3-2</h3>";

// The regex that will cut down the <hx>something</hx>
const regex = /<h[0-9]{1}>(.*?)<\/h[0-9]{1}>/g;

// We get the matches now
const matches = str.match(regex);

// We match the hx togethers as requested
const matchesSorted = Object.values(matches.reduce((tmp, x) => {
  // We get the number behind hx ---> the x
  const hNumber = x[2];

  // If the container do not exist, create it
  if (!tmp[hNumber]) {
    tmp[hNumber] = [];
  }

  // Push the new parsed content into the array
  // 4 is to start after <hx>
  // length - 9 is to get all except <hx></hx>
  tmp[hNumber].push(x.substr(4, x.length - 9));

  return tmp;
}, {}));

console.log(matchesSorted);


我们需要添加 .replace().trim() 来去除不必要的 \n空格

使用以下代码片段

// The string you need to parse
const str = "\
 <h1>h1-1\n\
 </h1>\
 <h2>  h2-1</h2>\
 <h3>h3-1</h3>\
 <p>something</p>\
 <h1>h1-2  </h1>\
 <h2>h2-2 \n\
 </h2>\
 <h3>h3-2</h3>";

// Remove all unwanted \n
const preparedStr = str.replace(/(\r\n\t|\n|\r\t)/gm, "");

// The regex that will cut down the <hx>something</hx>
const regex = /<h[0-9]{1}>(.*?)<\/h[0-9]{1}>/g;

// We get the matches now
const matches = preparedStr.match(regex);

// We match the hx togethers as requested
const matchesSorted = Object.values(matches.reduce((tmp, x) => {
  // We get the number behind hx ---> the x
  const hNumber = x[2];

  // If the container do not exist, create it
  if (!tmp[hNumber]) {
    tmp[hNumber] = [];
  }

  // Push the new parsed content into the array
  // 4 is to start after <hx>
  // length - 9 is to get all except <hx></hx>
  // call trim() to remove unwanted spaces
  tmp[hNumber].push(x.substr(4, x.length - 9).trim());

  return tmp;
}, {}));

console.log(matchesSorted);


2

我使用 JQuery 编写了这段代码。(请不要 DV。也许后面有人需要一个 JQuery 的答案)

这个递归函数创建字符串的 li,如果一个项目有子项,它会将它们转换为 ol

const str =
  "<div><h1>h1-1</h1><h2>h2-1</h2><h3>h3-1</h3></div><p>something</p><h1>h1-2</h1><h2>h2-2</h2><h3>h3-2</h3>";

function strToList(stri) {
  const tags = $(stri);

  function partToList(el) {
    let output = "<li>";
    if ($(el).children().length) {
      output += "<ol>";
      $(el)
        .children()
        .each(function() {
          output += partToList($(this));
        });
      output += "</ol>";
    } else {
      output += $(el).text();
    }
    return output + "</li>";
  }

  let output = "<ol>";

  tags.each(function(itm) {
    output += partToList($(this));
  });

  return output + "</ol>";
}

$("#output").append(strToList(str));
li {
  padding: 10px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div id="output"></div>

这段代码可以轻松转换为纯JS。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接