regex 如何使用JavaScript将节点中的部分文本换行

mcdcgff0  于 2022-12-05  发布在  Java
关注(0)|答案(5)|浏览(112)

我有一个很有挑战性的问题要解决。我正在编写一个脚本,该脚本将正则表达式作为输入。然后,该脚本在文档中查找该正则表达式的所有匹配项,并将每个匹配项 Package 在自己的<span>元素中。困难的部分是,文本是一个格式化的html文档,因此我的脚本需要在DOM中导航,并将正则表达式一次应用于多个文本节点。同时如果需要的话,计算出在哪里必须拆分文本节点。
例如,对于捕获以大写字母开头并以句点结尾的完整句子的正则表达式,此文档:

<p>
  <b>HTML</b> is a language used to make <b>websites.</b>
  It was developed by <i>CERN</i> employees in the early 90s.
</p>

理想情况下会变成这样:

<p>
  <span><b>HTML</b> is a language used to make <b>websites.</b></span>
  <span>It was developed by <i>CERN</i> employees in the early 90s.</span>
</p>

然后,脚本应返回所有已创建跨度的列表。
我已经有了一些代码,它可以找到所有的文本节点,并将它们存储在一个列表中,沿着它们在整个文档中的位置和深度。你不需要真正理解这些代码来帮助我,它的递归结构可能会有点混乱。我不知道如何做的第一部分是弄清楚哪些元素应该包含在span中。

function findTextNodes(node, depth = -1, start = 0) {
  let list = [];

  if (node.nodeType === Node.TEXT_NODE) {
    list.push({ node, depth, start });
  } else {
    for (let i = 0; i < node.childNodes.length; ++i) {
      list = list.concat(findTextNodes(node.childNodes[i], depth+1, start));
      if (list.length) {
        start += list[list.length-1].node.nodeValue.length;
      }
    }
  }

  return list;
}

我想我将把所有的文档做成一个字符串,在其中运行正则表达式,并使用列表找到哪些节点对应于正则表达式匹配,然后相应地拆分文本节点。
但是,当我有这样一个文档时,问题就出现了:

<p>
  This program is <a href="beta.html">not stable yet. Do not use this in production yet.</a>
</p>

有一个句子开始于<a>标记之外,但结束于标记之内。现在我不希望脚本将该链接拆分为两个标记。在更复杂的文档中,如果这样做,可能会破坏页面。代码可以将两个句子 Package 在一起:

<p>
  <span>This program is <a href="beta.html">not stable yet. Do not use this in production yet.</a></span>
</p>

或者将每个部分 Package 在其自己的元素中:

<p>
  <span>This program is </span>
  <a href="beta.html">
    <span>not stable yet.</span>
    <span>Do not use this in production yet.</span>
  </a>
</p>

可能有一个参数来指定它应该做什么。我只是不确定如何计算出何时将发生不可能的剪切,以及如何从中恢复。

当我的子元素中有空格时,就会出现另一个问题,如下所示

<p>This is a <b>sentence. </b></p>

从技术上讲,正则表达式匹配将在句点之后、<b>标记结束之前结束。然而,最好将空格视为匹配的一部分,并将其如下 Package :

<p><span>This is a <b>sentence. </b></span></p>

不如这样:

<p><span>This is a </span><b><span>sentence.</span> </b></p>

但这是一个小问题,毕竟,我可以允许在正则表达式中包含额外的空格。
我知道这听起来像是一个“为我做”的问题,也不是我们每天在SO上看到的那种快速问题,但我已经在这个问题上停留了一段时间,这是我正在做的一个开源库。解决这个问题是最后一个障碍。如果你认为另一个SE网站最适合这个问题,请重定向我。

b1payxdu

b1payxdu1#

这里有两种方法可以解决这个问题。
我不知道下面的代码是否 * 完全 * 符合您的需要。这是一个足够简单的解决方案,但至少它没有使用RegEx来操作HTML标记。它对原始文本执行模式匹配,然后使用DOM来操作内容。
第一次接近
这种方法只为每个匹配创建一个<span>标记,利用了一些不太常用的浏览器API。

  • (请参阅演示下面的此方法的主要问题,如果不确定,请使用第二种方法)*。

Range类表示一个文本片段。它有一个surroundContents函数,可以让你在元素中 Package 一个范围。除了它有一个警告:
该方法近似等价于newNode.appendChild(range.extractContents()); range.insertNode(newNode),包围后,范围的边界点包括newNode

**但是,如果Range只使用一个边界点拆分非Text节点,则会引发异常。**也就是说,与上面的替代方法不同,如果存在部分选择的节点,则不会克隆这些节点,相反,操作将失败。

嗯,MDN中提供了解决方法,所以一切都很好。
这里有一个算法:

  • 创建一个Text节点列表,并在文本中保留它们的起始索引
  • 连接这些节点的值以获得text
  • 在文本中查找匹配项,对于每个匹配项:
  • 查找匹配的起始节点和结束节点,将节点的起始索引与匹配位置进行比较
  • 在比赛上创建Range
  • 让浏览器使用上面的技巧来完成这些肮脏的工作
  • 自上次操作更改DOM以来重建节点列表

以下是我的实现和演示:
第一个
好了,这就是 lazy 方法,不幸的是,它在某些情况下不起作用。如果你 * 只 * 突出显示内联元素,它会很好地工作,但是当沿着有块元素时,它会中断,因为extractContents函数的以下属性:
克隆部分选定的节点以包括使文档片段有效所需的父标记。
这很糟糕。它只会复制块级节点。如果你想看看它是如何崩溃的,请尝试前面的baz\s+HTML正则表达式演示。
第二次接近
这种方法迭代匹配的节点,同时创建<span>标记。
整个算法很简单,因为它只是将每个匹配节点 Package 在自己的<span>中,但这意味着我们必须处理部分匹配的文本节点,这需要更多的工作。
如果文本节点部分匹配,则使用splitText函数拆分它:
拆分后,当前节点包含指定偏移点之前的所有内容,新创建的相同类型的节点包含剩余文本。新创建的节点返回给调用方。
第一个
如果你需要最小化<span>标签的数量,可以通过扩展这个函数来实现,但是我想让它保持简单。

kpbwa7wx

kpbwa7wx3#

我将使用“平面DOM”表示来完成此类任务。
在平面DOM中此段落

<p>abc <a href="beta.html">def. ghij.</p>

将由两个向量表示:

chars: "abc def. ghij.",
props:  ....aaaaaaaaaa,

您将在chars上使用normal regexp来标记props向量上的跨度区域:

chars: "abc def. ghij."
props:  ssssaaaaaaaaaa  
            ssss sssss

我在这里用的是示意图表示,它的真实的结构是一个数组的数组:

props: [
  [s],
  [s],
  [s],
  [s],
  [a,s],
  [a,s],
  ...
]

转换树DOM<->平面DOM可以使用简单状态自动机。
最后,将平面DOM转换为树DOM,如下所示:

<p><s>abc </s><a href="beta.html"><s>def.</s> <s>ghij.</s></p>

以防万一:我在我的HTML WYSIWYG编辑器中使用这种方法。

am46iovg

am46iovg4#

正如大家已经说过的,这是一个学术问题,因为这不应该是你真正做的方式。
编辑:我想我现在明白了要点。

function myReplace(str) {
  myRegexp = /((^<[^>*]>)+|([^<>\.]*|(<[^\/>]*>[^<>\.]+<\/[^>]*>)+)*[^<>\.]*\.\s*|<[^>]*>|[^\.<>]+\.*\s*)/g; 
  arr = str.match(myRegexp);
  var out = "";
  for (i in arr) {
var node = arr[i];
if (node.indexOf("<")===0) out += node;
else out += "<span>"+node+"</span>"; // Here is where you would run whichever 
                                     // regex you want to match by
  }
  document.write(out.replace(/</g, "&lt;").replace(/>/g, "&gt;")+"<br>");
  console.log(out);
}

myReplace('<p>This program is <a href="beta.html">not stable yet. Do not use this in production yet.</a></p>');
myReplace('<p>This is a <b>sentence. </b></p>');
myReplace('<p>This is a <b>another</b> and <i>more complex</i> even <b>super complex</b> sentence.</p>');
myReplace('<p>This is a <b>a sentence</b>. Followed <i>by</i> another one.</p>');
myReplace('<p>This is a <b>an even</b> more <i>complex sentence. </i></p>');

/* Will output:
<p><span>This program is </span><a href="beta.html"><span>not stable yet. </span><span>Do not use this in production yet.</span></a></p>
<p><span>This is a </span><b><span>sentence. </span></b></p>
<p><span>This is a <b>another</b> and <i>more complex</i> even <b>super complex</b> sentence.</span></p>
<p><span>This is a <b>a sentence</b>. </span><span>Followed <i>by</i> another one.</span></p>
<p><span>This is a </span><b><span>an even</span></b><span> more </span><i><span>complex sentence. </span></i></p>
*/
rmbxnbpk

rmbxnbpk5#

我已经花了很长时间实现了这个线程中给出的所有方法。
1.节点迭代器

  1. Html解析
    1.平面圆顶
    对于任何一种方法,你都必须找到一种技术,把整个html拆分成句子,然后打包成span(有些人可能想把单词放在span中)。一旦我们这样做了,我们就会遇到性能问题(我应该说像我这样的初学者会遇到性能问题)。

性能瓶颈

我无法将这种方法扩展到70 k-200 k单词,而且仍然在毫秒内完成。
对于包含文本节点和不同元素的复杂html页面,我们很快就遇到了麻烦,而且随着技术债务的不断增加。

最佳方法:Mark.js(根据我的说法)
**注意:**如果你做得正确,你可以处理任何数量的单词。

只需使用Ranges,我想推荐Mark.js和以下示例:

var instance = new Mark(document.body);
instance.markRanges([{
    start: 15,
    length: 5
}, {
    start: 25:
    length: 8
}]); /

这样,我们就可以将整个body.textContent视为字符串,并一直突出显示substring
这里没有修改DOM结构,你可以很容易地修复复杂的用例,技术债务不会随着if和else的增加而增加。
此外,一旦文本被html5 mark标记高亮显示,您可以后处理这些标记以找出边界矩形。
如果你只是想把html文档拆分成words/chars/lines和更多的文档,也可以看看Splitting.js...但是这种方法的一个缺点是Splitting.js会折叠文档中的额外空格,所以我们会丢失一些信息。

  • 谢谢-谢谢

相关问题