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

Dom*_*ino 46 html javascript regex algorithm

我有一个具有挑战性的问题要解决.我正在编写一个以正则表达式作为输入的脚本.然后,此脚本在文档中查找此正则表达式的所有匹配项,并将每个匹配项包装在其自己的<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>
Run Code Online (Sandbox Code Playgroud)

会变成这样:

<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>
Run Code Online (Sandbox Code Playgroud)

然后该脚本返回所有已创建的跨度的列表.

我已经有一些代码可以找到所有文本节点,并将它们存储在列表中,以及它们在整个文档中的位置及其深度.你真的不需要理解代码来帮助我及其递归结构可能有点令人困惑.Ť 他我不知道该怎么办第一部分是搞清楚哪些元素应包括在范围之内.

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;
}
Run Code Online (Sandbox Code Playgroud)

我想我将从所有文档中创建一个字符串,通过它运行正则表达式并使用列表查找哪些节点对应于巫法正则表达式匹配,然后相应地拆分文本节点.

但是当我有这样的文档时,问题就出现了:

<p>
  This program is <a href="beta.html">not stable yet. Do not use this in production yet.</a>
</p>
Run Code Online (Sandbox Code Playgroud)

有一个句子从<a>标签之外开始但在其内部结束.现在我不希望脚本将该链接拆分为两个标记.在一个更复杂的文档中,它可能会破坏页面.代码可以将两个句子包装在一起:

<p>
  <span>This program is <a href="beta.html">not stable yet. Do not use this in production yet.</a></span>
</p>
Run Code Online (Sandbox Code Playgroud)

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

<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>
Run Code Online (Sandbox Code Playgroud)

可以有一个参数来指定它应该做什么.我只是不确定如何弄清楚何时会发生不可能的切割,以及如何从中恢复.

当我在像这样的子元素中有空格时,会出现另一个问题:

<p>This is a <b>sentence. </b></p>
Run Code Online (Sandbox Code Playgroud)

从技术上讲,正则表达式匹配将在<b>标记结束之前的句点之后结束.但是,将空间视为匹配的一部分会更好,并将其包装成如下:

<p><span>This is a <b>sentence. </b></span></p>
Run Code Online (Sandbox Code Playgroud)

比这个:

<p><span>This is a </span><b><span>sentence.</span> </b></p>
Run Code Online (Sandbox Code Playgroud)

但这是一个小问题.毕竟,我可以在正则表达式中包含额外的空格.

我知道这可能听起来像是"为我做的"问题,而不是我们每天在SO上看到的那种快速问题,但我已经坚持了一段时间而且它是一个开源库我正在尝试.解决这个问题是最后一个障碍.如果您认为另一个SE网站最适合此问题,请重定向我.

Luc*_*ski 34

这有两种方法可以解决这个问题.

我不知道以下内容是否完全符合您的需求.这是解决问题的简单方法,但至少它不使用RegEx来操作HTML标记.它对原始文本执行模式匹配,然后使用DOM来操作内容.


第一种方法

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

Range类表示文本片段.它有一个surroundContents函数,可以包含元素中的范围.除了它有一个警告:

这种方法几乎相当于newNode.appendChild(range.extractContents()); range.insertNode(newNode).在周围之后,范围的边界点包括newNode.

但是,如果仅使用其一个边界点Range拆分非Text节点,则会抛出异常.也就是说,与上面的替代方案不同,如果存在部分选择的节点,则不会克隆它们,而是操作将失败.

好吧,MDN中提供了解决方法,所以一切都很好.

所以这是一个算法:

  • 制作Text节点列表并在文本中保留其起始索引
  • 连接这些节点的值以获得 text
  • 查找文本和每个匹配的匹配项:

    • 找到匹配的起始节点和结束节点,将节点的起始索引与匹配位置进行比较
    • Range在比赛中创建一个
    • 让浏览器使用上面的技巧完成脏工作
    • 自上次操作更改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();
Run Code Online (Sandbox Code Playgroud)
.highlight {
  background-color: yellow;
  border: 1px solid orange;
  border-radius: 5px;
}

.section {
  border: 1px solid gray;
  padding: 10px;
  margin: 10px;
}
Run Code Online (Sandbox Code Playgroud)
<form class="section">
  RegEx: <input id="regex" type="text" value="[A-Z].*?\." /> <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>
Run Code Online (Sandbox Code Playgroud)

好吧,这是一种懒惰的方法,遗憾的是在某些情况下不起作用.如果您突出显示内联元素,它会很好用,但是由于extractContents函数的以下属性,在途中存在块元素时会中断:

克隆部分选定的节点以包括使文档片段有效所必需的父标记.

那很糟.它只会复制块级节点.baz\s+HTML如果你想看看它如何破坏,请尝试使用正则表达式的上一个演示.


第二种方法

此方法迭代匹配节点,<span>沿途创建标记.

整个算法很简单,因为它只包含每个匹配节点<span>.但这意味着我们必须处理部分匹配的文本节点,这需要更多的努力.

如果文本节点部分匹配,则使用以下splitText函数拆分:

在拆分之后,当前节点包含直到指定偏移点的所有内容,并且新创建的相同类型的节点包含剩余文本.新创建的节点将返回给调用者.

function highlight(element, regex) {
    var document = element.ownerDocument;
    
    var nodes = [],
        text = "",
        node,
        nodeIterator = document.createNodeIterator(element, NodeFilter.SHOW_TEXT, null, false);
        
    while (node = nodeIterator.nextNode()) {
        nodes.push({
            textNode: node,
            start: text.length
        });
        text += node.nodeValue
    }
    
    if (!nodes.length)
        return;

    var match;
    while (match = regex.exec(text)) {
        var matchLength = match[0].length;
        
        // Prevent empty matches causing infinite loops        
        if (!matchLength)
        {
            regex.lastIndex++;
            continue;
        }
        
        for (var i = 0; i < nodes.length; ++i) {
            node = nodes[i];
            var nodeLength = node.textNode.nodeValue.length;
            
            // Skip nodes before the match
            if (node.start + nodeLength <= match.index)
                continue;
        
            // Break after the match
            if (node.start >= match.index + matchLength)
                break;
            
            // Split the start node if required
            if (node.start < match.index) {
                nodes.splice(i + 1, 0, {
                    textNode: node.textNode.splitText(match.index - node.start),
                    start: match.index
                });
                continue;
            }
            
            // Split the end node if required
            if (node.start + nodeLength > match.index + matchLength) {
                nodes.splice(i + 1, 0, {
                    textNode: node.textNode.splitText(match.index + matchLength - node.start),
                    start: match.index + matchLength
                });
            }
            
            // Highlight the current node
            var spanNode = document.createElement("span");
            spanNode.className = "highlight";
            
            node.textNode.parentNode.replaceChild(spanNode, node.textNode);
            spanNode.appendChild(node.textNode);
        }
    }
}

// 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();
Run Code Online (Sandbox Code Playgroud)
.highlight {
  background-color: yellow;
}

.section {
  border: 1px solid gray;
  padding: 10px;
  margin: 10px;
}
Run Code Online (Sandbox Code Playgroud)
<form class="section">
  RegEx: <input id="regex" type="text" value="[A-Z].*?\." /> <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>
Run Code Online (Sandbox Code Playgroud)

对于我希望的大多数情况,这应该足够好了.如果您需要<span>通过扩展此功能来最小化标签数量,但我现在想保持简单.


MT0*_*MT0 6

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 );
Run Code Online (Sandbox Code Playgroud)
.sentence {
  text-decoration: underline wavy red;
}

.sentence-end {
  border-right: 1px solid red;
}
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)


c-s*_*ile 5

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

在平面 DOM 中这一段

<p>abc <a href="beta.html">def. ghij.</p>
Run Code Online (Sandbox Code Playgroud)

将由两个向量表示:

chars: "abc def. ghij.",
props:  ....aaaaaaaaaa, 
Run Code Online (Sandbox Code Playgroud)

您将使用普通的正则表达式chars来标记道具向量上的跨度区域:

chars: "abc def. ghij."
props:  ssssaaaaaaaaaa  
            ssss sssss
Run Code Online (Sandbox Code Playgroud)

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

props: [
  [s],
  [s],
  [s],
  [s],
  [a,s],
  [a,s],
  ...
]
Run Code Online (Sandbox Code Playgroud)

转换 tree-DOM <-> flat-DOM 可以使用简单的状态自动机。

最后,您将平面 DOM 转换为树 DOM,如下所示:

<p><s>abc </s><a href="beta.html"><s>def.</s> <s>ghij.</s></p>
Run Code Online (Sandbox Code Playgroud)

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


Jan*_*Jan 5

正如大家已经说过的那样,这更像是一个学术问题,因为这不应该是你这样做的方式.话虽如此,它似乎很有趣,所以这是一种方法.

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

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>
*/
Run Code Online (Sandbox Code Playgroud)

  • 就像我说的,这个答案纯粹是学术性的.可以用正则表达式完成吗?差不多.它可能会产生不可预见的副作用吗?大概.你真的应该使用任何其他方法吗?明确地.这是一个有趣的小谜题要解决吗?你打赌. (2认同)