我有一个很有挑战性的问题要解决。我正在编写一个脚本,该脚本将正则表达式作为输入。然后,该脚本在文档中查找该正则表达式的所有匹配项,并将每个匹配项 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网站最适合这个问题,请重定向我。
5条答案
按热度按时间b1payxdu1#
这里有两种方法可以解决这个问题。
我不知道下面的代码是否 * 完全 * 符合您的需要。这是一个足够简单的解决方案,但至少它没有使用RegEx来操作HTML标记。它对原始文本执行模式匹配,然后使用DOM来操作内容。
第一次接近
这种方法只为每个匹配创建一个
<span>
标记,利用了一些不太常用的浏览器API。Range
类表示一个文本片段。它有一个surroundContents
函数,可以让你在元素中 Package 一个范围。除了它有一个警告:该方法近似等价于
newNode.appendChild(range.extractContents()); range.insertNode(newNode)
,包围后,范围的边界点包括newNode
。**但是,如果
Range
只使用一个边界点拆分非Text
节点,则会引发异常。**也就是说,与上面的替代方法不同,如果存在部分选择的节点,则不会克隆这些节点,相反,操作将失败。嗯,MDN中提供了解决方法,所以一切都很好。
这里有一个算法:
Text
节点列表,并在文本中保留它们的起始索引text
Range
以下是我的实现和演示:
第一个
好了,这就是 lazy 方法,不幸的是,它在某些情况下不起作用。如果你 * 只 * 突出显示内联元素,它会很好地工作,但是当沿着有块元素时,它会中断,因为
extractContents
函数的以下属性:克隆部分选定的节点以包括使文档片段有效所需的父标记。
这很糟糕。它只会复制块级节点。如果你想看看它是如何崩溃的,请尝试前面的
baz\s+HTML
正则表达式演示。第二次接近
这种方法迭代匹配的节点,同时创建
<span>
标记。整个算法很简单,因为它只是将每个匹配节点 Package 在自己的
<span>
中,但这意味着我们必须处理部分匹配的文本节点,这需要更多的工作。如果文本节点部分匹配,则使用
splitText
函数拆分它:拆分后,当前节点包含指定偏移点之前的所有内容,新创建的相同类型的节点包含剩余文本。新创建的节点返回给调用方。
第一个
如果你需要最小化
<span>
标签的数量,可以通过扩展这个函数来实现,但是我想让它保持简单。pepwfjgg2#
第一个
kpbwa7wx3#
我将使用“平面DOM”表示来完成此类任务。
在平面DOM中此段落
将由两个向量表示:
您将在
chars
上使用normal regexp来标记props向量上的跨度区域:我在这里用的是示意图表示,它的真实的结构是一个数组的数组:
转换树DOM<->平面DOM可以使用简单状态自动机。
最后,将平面DOM转换为树DOM,如下所示:
以防万一:我在我的HTML WYSIWYG编辑器中使用这种方法。
am46iovg4#
正如大家已经说过的,这是一个学术问题,因为这不应该是你真正做的方式。
编辑:我想我现在明白了要点。
rmbxnbpk5#
我已经花了很长时间实现了这个线程中给出的所有方法。
1.节点迭代器
1.平面圆顶
对于任何一种方法,你都必须找到一种技术,把整个html拆分成句子,然后打包成span(有些人可能想把单词放在span中)。一旦我们这样做了,我们就会遇到性能问题(我应该说像我这样的初学者会遇到性能问题)。
性能瓶颈
我无法将这种方法扩展到70 k-200 k单词,而且仍然在毫秒内完成。
对于包含文本节点和不同元素的复杂html页面,我们很快就遇到了麻烦,而且随着技术债务的不断增加。
最佳方法:Mark.js(根据我的说法)
**注意:**如果你做得正确,你可以处理任何数量的单词。
只需使用
Ranges
,我想推荐Mark.js和以下示例:这样,我们就可以将整个
body.textContent
视为字符串,并一直突出显示substring
。这里没有修改DOM结构,你可以很容易地修复复杂的用例,技术债务不会随着if和else的增加而增加。
此外,一旦文本被html5
mark
标记高亮显示,您可以后处理这些标记以找出边界矩形。如果你只是想把html文档拆分成
words/chars/lines
和更多的文档,也可以看看Splitting.js
...但是这种方法的一个缺点是Splitting.js
会折叠文档中的额外空格,所以我们会丢失一些信息。