如何从文本编辑器生成的 html 中删除不必要的标签

How to remove unnecessary tags from html generate from text editor

下面是文本编辑器从 word 文档自动生成的 html 脚本,summernote

var html = `
<p>
   <b>
   <br>
   </b>
</p>
<p>
   <b>អ្នកធានា</b>
</p>
<p>
   <b>ឈ្មោះ: ……………………………</b>
</p>
<p>
   <b>អត្តសញ្ញាណប័ណ្ណលេខៈ………………...............
   <span style="white-space:pre"></span>..........................................
   </b>
</p>
<p>
   <b>
   <span style="white-space:pre"></span>ហត្ថលេខានិង ស្នាមមេដៃស្តាំ
   <span style="white-space:pre"></span>
   </b>
</p>
<p>
   <b>
   <br>
   </b>
</p>
<p>`;

在它为我生成 hmlt 代码后,我尝试通过删除不必要的空标签和不包含任何值的标签来清理它。

所以,我尝试了如下的 JS 脚本:

html.replace('<p><br></p>', ''); // remove unneccessary tage
html.replace('&nbsp;', ''); // remove &nbsp; space
console.log(html);

但是,上面的JS脚本没有任何变化后,空的和不需要的标签仍然存在。

我不知道为什么它不起作用,但我只是尝试了非常简单的替换 '<p><br></p>not replaced'.replace('<p><br></p>',''),它工作得很好。

上面有什么问题吗?我怎样才能从上面删除所有不必要的标签?谢谢。

您的 replace 行不起作用,因为它与您的 HTML 的确切结构不匹配,并且没有考虑标签之间的空格。您可以在 replace 调用中使用 RegExp 来处理空白,如下所示:

html.replace(/<p>\s*<br>\s*<\/p>/, '');
//           /                    start of the regex literal
//            <p>                 a literal "<p>"
//               \s               any whitespace character
//                 *              previous char, zero or more times
//                  <br>          a literal "br"
//                      \s        any whitespace character
//                        *       previous char, zero or more times
//                         <\/p>  a literal "</p>" (with escaped slash)
//                              / end of regex

那会匹配 <p><br></p>,但是 <p> 中的 <b> 犯规了。您可以制作越来越复杂的正则表达式来处理越来越深奥的情况,但是 that way lies madness and isn't possible in the general case.

相反,我们可以将生成的 HTML 拉入 DocumentFragment。然后我们可以将它作为一个 DOM 树来处理,而不是一个字符串:

const template = document.createElement('template');
template.innerHTML = html;
const fragment = template.content;
removeUselessNodes(fragment); // we'll need to write this one

<template> HTMLTemplateElement helps us here, because we can assign an HTML string to its innerHTML property and pull it back out as a DocumentFragment from the content 属性。如果我们更改 DocumentFragment 的结构,这些更改将反映在 innerHTML 属性 中。*

*我找不到支持我的文档,但它适用于 Firefox 和 Chromium。

现在我们需要实际移除 "unnecessary, empty tags, and tags that [do] not contain any value." 我们将定义 无用节点 来帮助做到这一点:

  • Comment nodes are useless.
  • Text nodes that are empty or contain only whitespace are useless.
  • Non-void element nodes whose child nodes contain only useless nodes or <br> elements are useless.

All other nodes are not useless.

我们需要一个函数来识别和删除无用的节点。由于我们要在整个树中搜索无用节点,因此我们将在节点的子节点上递归调用该函数:

function removeUselessNodes(node) {
  for (let i = node.childNodes.length - 1; i >= 0; --i) {
    removeUselessNodes(node.childNodes.item(i));
  }

我们反向迭代子节点,因为 Node.childNodes 是一个活动列表,我们将从中删除元素。循环不知道我们正在做的改变,如果我们继续前进,它会跳过元素。从列表末尾删除元素不会中断向后迭代循环。我们首先执行递归调用,因为它可以更容易地检查最后一个无用节点条件。

所有的树遍历都完成后,我们可以从无用节点条件开始。让我们一一道来:

  • Comment nodes are useless.

这个很简单。 Nodes 有一个 属性 表示它们的类型,nodeType。我们可以检查它并删除该节点,如果它是评论:

  if (node.nodeType === Node.COMMENT_NODE) {
    node.remove();
    return;
  }

我们return删除了无用的节点后立即;没有什么可做的了。下一篇:

  • Text nodes that are empty or contain only whitespace are useless.

"[E]mpty or contain[s] only whitespace" 是另一种说法 "doesn't contain non-whitespace",我们可以用 RegExp.test.

来测试
  if (
    node.nodeType === Node.TEXT_NODE
    && !/\S/.test(node.textContent)
  ) {
    node.remove();
    return;
  }

\s是空白字符,\S(注意大小写)是非空白字符。)

最后一个测试需要一点拆包:

  • Non-void element nodes whose child nodes contain only useless nodes or <br> elements are useless.

空元素是不能有子元素的元素:比如 <img>s 和 <hr>s。它们并非毫无用处;他们有自己的意义。出于我们的目的,非空元素需要有意义的子元素才能有意义。 <p> 本身只会在页面上腾出一些空间。它的子文本节点是文本的来源。 <br> 在与其他节点相邻时并非毫无用处,但它本身不足以使其父节点有意义。

将其分解为单独的测试,我们得到

  • 必须是元素节点
  • 必须是非空的
  • 子节点必须只包含无用节点或 <br> 个元素

我们之前测试过节点类型:

  if (
    node.nodeType === Node.ELEMENT_NODE

在 JavaScript 中没有方便的方法来检查是否为空,但 HTML5 规范包括 a list of void elements we can check against with the Element.tagName 属性:

    && ![
      'AREA',
      'BASE',
      'BR',
      'COL',
      'EMBED',
      'HR',
      'IMG',
      'INPUT',
      'LINK',
      'META',
      'PARAM',
      'SOURCE',
      'TRACK',
      'WBR'
    ].includes(node.tagName)

由于我们已经从该节点中删除了所有无用的子节点,如果该节点的所有子节点都是 <br> 元素,则该节点通过第三次测试。 childNodes 是一个 NodeList,它没有 every 方法,但是对于索引为 0 的元素和一个 length 属性,我们可以调用 Array 的 every 方法:

    && Array.prototype.every.call(node.childNodes, n => n.tagName === 'BR')
  ) {
    node.remove();
    return;
  }
}

至此,所有 fragment 无用的节点都被删除了。您可以从 template.innerHTML 获得结果 HTML,或者直接将它发送到另一个元素 document.adoptNode:

const adoptedNode = document.adoptNode(fragment);
document.querySelector('#destination').appendChild(adoptedNode);

综合起来:

var html = `
<p>
   <b>
   <br>
   </b>
</p>
<p>
   <b>អ្នកធានា</b>
</p>
<p>
   <b>ឈ្មោះ: ……………………………</b>
</p>
<p>
   <b>អត្តសញ្ញាណប័ណ្ណលេខៈ………………...............
   <span style="white-space:pre"></span>..........................................
   </b>
</p>
<p>
   <b>
   <span style="white-space:pre"></span>ហត្ថលេខានិង ស្នាមមេដៃស្តាំ
   <span style="white-space:pre"></span>
   </b>
</p>
<p>
   <b>
   <br>
   </b>
</p>
<p>`;

function removeUselessNodes(node) {
  for (let i = node.childNodes.length - 1; i >= 0; --i) {
    removeUselessNodes(node.childNodes.item(i));
  }

  if (node.nodeType === Node.COMMENT_NODE) {
    node.remove();
    return;
  }
  
  if (
    node.nodeType === Node.TEXT_NODE
    && !/\S/.test(node.textContent)
  ) {
    node.remove();
    return;
  }

  if (
    node.nodeType === Node.ELEMENT_NODE
    && ![
      'AREA',
      'BASE',
      'BR',
      'COL',
      'EMBED',
      'HR',
      'IMG',
      'INPUT',
      'LINK',
      'META',
      'PARAM',
      'SOURCE',
      'TRACK',
      'WBR'
    ].includes(node.tagName)
    && Array.prototype.every.call(node.childNodes, n => n.tagName === 'BR')
  ) {
    node.remove();
    return;
  }
}

const template = document.createElement('template');
template.innerHTML = html;
const fragment = template.content;
removeUselessNodes(fragment);

document.querySelector('#rawHTML').value = template.innerHTML;
const adoptedNode = document.adoptNode(fragment);
document.querySelector('#destination').appendChild(adoptedNode);
#rawHTML {
  width: 95vw;
  height: 10em;
}
<textarea id="rawHTML"></textarea>
<div id="destination"></div>