如何用JavaScript包装节点中的文本的一部分

我有一个具有挑战性的问题要解决。 我正在研究一个以正则expression式作为input的脚本。 这个脚本然后在文档中find这个正则expression式的所有匹配,并将每个匹配包装在它自己的<span>元素中。 最难的部分是文本是一个格式化的HTML文档,所以我的脚本需要浏览DOM,并一次性在多个文本节点上应用正则expression式,同时根据需要找出需要分割文本节点的位置。

例如,用一个正则expression式来捕获以大写字母开始并以句点结尾的完整句子,本文档:

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

该脚本然后返回所有创build的跨度的列表。

我已经有了一些代码,可以find所有的文本节点,并将它们存储在一个列表中,以及它们在整个文档中的位置及其深度。 你并不需要理解代码来帮助我,它的recursion结构可能会有点混乱。 第一部分,我不知道该怎么做是找出哪些元素应该包含在范围内。

 function SmartNode(node, depth, start) { this.node = node; this.depth = depth; this.start = start; } function findTextNodes(node, depth, start) { var list = []; var start = start || 0; depth = (typeof depth !== "undefined" ? depth : -1); if(node.nodeType === Node.TEXT_NODE) { list.push(new SmartNode(node, depth, start)); } else { for(var 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; } 

我想我会从所有文档中创build一个string,通过它运行正则expression式,并使用列表来查找哪些节点对应于女巫正则expression式匹配,然后相应地分割文本节点。

但是当我有这样一个文档时,问题就到了:

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

有一个句子是从<a>标签之外开始的,但是在里面结束。 现在我不希望脚本将链接分成两个标签。 在一个更复杂的文件中,如果有的话可能会毁掉页面。 代码可以包装两个句子在一起:

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

或者只是将每个部分包装在自己的元素中:

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

从技术上讲,正则expression式匹配会在<b>标签结束之前的期间结束。 然而,将空间视为比赛的一部分,并将其包裹如下:

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

比这个:

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

但这是一个小问题。 毕竟,我可以允许在正则expression式中包含额外的空白。

我知道这可能听起来像是“为我而做”的问题,而不是我们日常所见到的那种快速的问题,但是我一直坚持这一点,这是一个开源的库我正在尝试。 解决这个问题是最后的障碍。 如果您认为另一个SE网站最适合这个问题,请redirect我。

这里有两个方法来处理这个问题。

我不知道下面是否会完全符合你的需求。 这是一个足够简单的解决scheme,但至less它不使用RegEx来操纵HTML标签 。 它对原始文本执行模式匹配,然后使用DOM来操作内容。


第一种方法

这种方法每个匹配只创build一个<span>标签,利用一些不太常见的浏览器API。
(请参阅演示下方的这种方法的主要问题,如果不确定,请使用第二种方法)

Range类代表一个文本片段。 它有一个surroundContents函数,可以让你在一个元素中包装一个范围。 除了有一个警告:

这个方法几乎等于newNode.appendChild(range.extractContents()); range.insertNode(newNode) newNode.appendChild(range.extractContents()); range.insertNode(newNode) 。 周围后,范围的边界点包括newNode

但是,如果Range将一个非Text节点与其边界点分开,将引发exception。 也就是说,与上面的替代scheme不同,如果有部分选定的节点,它们将不会被克隆,而是操作失败。

那么,MDN提供了解决方法,所以一切都很好。

所以这是一个algorithm:

  • 制作Text节点列表,并在Text中保留其起始索引
  • 连接这些节点的值来获取text
  • 在文本上查找匹配,并为每个匹配:

    • find匹配的开始和结束节点,比较节点的起始索引和匹配位置
    • 在比赛中创build一个Range
    • 让浏览器使用上面的技巧来完成肮脏的工作
    • 重build节点列表,因为最后一个操作改变了DOM

这是我的演示实现:

 function highlight(element, regex) { var document = element.ownerDocument; var getNodes = function() { var nodes = [], offset = 0, node, nodeIterator = document.createNodeIterator(element, NodeFilter.SHOW_TEXT, null, false); while (node = nodeIterator.nextNode()) { nodes.push({ textNode: node, start: offset, length: node.nodeValue.length }); offset += node.nodeValue.length } return nodes; } var nodes = getNodes(nodes); if (!nodes.length) return; var text = ""; for (var i = 0; i < nodes.length; ++i) text += nodes[i].textNode.nodeValue; var match; while (match = regex.exec(text)) { // Prevent empty matches causing infinite loops if (!match[0].length) { regex.lastIndex++; continue; } // Find the start and end text node var startNode = null, endNode = null; for (i = 0; i < nodes.length; ++i) { var node = nodes[i]; if (node.start + node.length <= match.index) continue; if (!startNode) startNode = node; if (node.start + node.length >= match.index + match[0].length) { endNode = node; break; } } var range = document.createRange(); range.setStart(startNode.textNode, match.index - startNode.start); range.setEnd(endNode.textNode, match.index + match[0].length - endNode.start); var spanNode = document.createElement("span"); spanNode.className = "highlight"; spanNode.appendChild(range.extractContents()); range.insertNode(spanNode); nodes = getNodes(); } } // Test code var testDiv = document.getElementById("test-cases"); var originalHtml = testDiv.innerHTML; function test() { testDiv.innerHTML = originalHtml; try { var regex = new RegExp(document.getElementById("regex").value, "g"); highlight(testDiv, regex); } catch(e) { testDiv.innerText = e; } } document.getElementById("runBtn").onclick = test; test(); 
 .highlight { background-color: yellow; border: 1px solid orange; border-radius: 5px; } .section { border: 1px solid gray; padding: 10px; margin: 10px; } 
 <form class="section"> RegEx: <input id="regex" type="text" value="[AZ].*?\." /> <button id="runBtn">Highlight</button> </form> <div id="test-cases" class="section"> <div>foo bar baz</div> <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> This program is <a href="beta.html">not stable yet. Do not use this in production yet.</a> </p> <div>foo bar baz</div> </div> 

正如大家已经说过的,这更像是一个学术问题,因为这不应该是你做这件事的方式。 这就是说,这似乎很有趣,所以这里有一个方法。

编辑:我想我现在得到了它的要点。

 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> */ 
 function parseText( element ){ var stack = [ element ]; var group = false; var re = /(?!\s|$).*?(\.|$)/; while ( stack.length > 0 ){ var node = stack.shift(); if ( node.nodeType === Node.TEXT_NODE ) { if ( node.textContent.trim() != "" ) { var match; while( node && (match = re.exec( node.textContent )) ) { var start = group ? 0 : match.index; var length = match[0].length + match.index - start; if ( start > 0 ) { node = node.splitText( start ); } var wrapper = document.createElement( 'span' ); var next = null; if ( match[1].length > 0 ){ if ( node.textContent.length > length ) next = node.splitText( length ); group = false; wrapper.className = "sentence sentence-end"; } else { wrapper.className = "sentence"; group = true; } var parent = node.parentNode; var sibling = node.nextSibling; wrapper.appendChild( node ); if ( sibling ) parent.insertBefore( wrapper, sibling ); else parent.appendChild( wrapper ); node = next; } } } else if ( node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.DOCUMENT_NODE ) { stack.unshift.apply( stack, node.childNodes ); } } } parseText( document.body ); 
 .sentence { text-decoration: underline wavy red; } .sentence-end { border-right: 1px solid red; } 
 <p>This is a sentence. This is another sentence.</p> <p>This sentence has <strong>emphasis</strong> inside it.</p> <p><span>This sentence spans</span><span> two elements.</span></p> 

我会使用“平面的DOM”表示这样的任务。

在平面DOM这一段

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

将由两个向量表示:

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

你将使用普通的正则expression式来标记道具vector上的跨度区域:

 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所见即所得的编辑器中使用这种方法。