JavaScript正则表达式用于替换非HTML属性中的文本

16
我需要一个 JavaScript 正则表达式来将给定的一组单词用给定的开始标签(即 <span>)和结束标签(即 </span>)包装起来,但只有当这些单词实际是页面上“可见文本”时才进行包装,而不是在 HTML 属性(如链接的 title 标签或在 <script></script> 块内)中。
我已经创建了一个带有基本设置的 JS Fiddle: http://jsfiddle.net/4YCR6/1/

正如其他人所说,通常不建议使用正则表达式处理HTML。但有些情况下这可能是最简单的方法。可以尝试这个:更新的jsfiddlerubular上。 - morja
请见https://dev59.com/rE_Sa4cB1Zd3GeqP8wsd - Ryan
2个回答

41

使用正则表达式可靠地解析HTML太过复杂。

如果你想在客户端进行操作,你可以创建一个文档片段或未连接的DOM节点(两者都不会显示在任何地方),并用你的HTML字符串来初始化它,然后遍历生成的DOM树并处理文本节点。(或使用库来帮助你做到这一点,虽然实际上很简单。)

这里是一个DOM遍历示例。这个示例略微比你的问题简单,因为它只更新了文本,而没有向结构中添加新元素(将文本的一部分用标签包裹涉及到更新结构),但它应该可以让你开始工作。最后注释需要更改的内容。

var html =
    "<p>This is a test.</p>" +
    "<form><input type='text' value='test value'></form>" +
    "<p class='testing test'>Testing here too</p>";
var frag = document.createDocumentFragment();
var body = document.createElement('body');
var node, next;

// Turn the HTML string into a DOM tree
body.innerHTML = html;

// Walk the dom looking for the given text in text nodes
walk(body);

// Insert the result into the current document via a fragment
node = body.firstChild;
while (node) {
  next = node.nextSibling;
  frag.appendChild(node);
  node = next;
}
document.body.appendChild(frag);

// Our walker function
function walk(node) {
  var child, next;

  switch (node.nodeType) {
    case 1:  // Element
    case 9:  // Document
    case 11: // Document fragment
      child = node.firstChild;
      while (child) {
        next = child.nextSibling;
        walk(child);
        child = next;
      }
      break;
    case 3: // Text node
      handleText(node);
      break;
  }
}

function handleText(textNode) {
  textNode.nodeValue = textNode.nodeValue.replace(/test/gi, "TEST");
}

实例

你需要修改的地方在于handleText函数中。具体来说,你需要:

  • 找到nodeValue字符串中每个单词开头的索引。
  • 使用Node#splitText将文本节点分成最多三个文本节点(匹配文本之前的部分、匹配文本和匹配文本之后的部分)。
  • 使用document.createElement创建新的span元素(直接写 span = document.createElement('span'))。
  • 使用Node#insertBefore在第三个文本节点(包含匹配文本之后的文本)前插入新的span元素,如果你不需要创建第三个节点,因为你的匹配文本是在文本节点末尾,则将null作为refChild参数传递即可。
  • 使用Node#appendChild将第二个文本节点(包含匹配文本的那个)移动到span元素中。(无需先从其父级中删除,appendChild会为你完成。)

14
有趣的事实:将近五年后,他们在与约翰·奥利弗主持的《上周今夜秀》中使用了这个代码,用于Drumpfinator Chrome扩展程序。非常有趣! - T.J. Crowder
你也发现了吗?哦等等,这是你的答案吗?你被咨询过还是像我一样查看了扩展? - brace110
1
@brace110:一位非常友善的年轻女士查看了源代码后给我发了一封电子邮件。 :-) - T.J. Crowder
1
我自己也在类似的项目中使用它,当然要适当地进行归属。 :) - Stephen Tetreault
2
同时也支持将Cloud替换成Butt!https://github.com/panicsteve/cloud-to-butt/blob/master/Source/content_script.js - Craig Dennis

13

T.J. Crowder 的回答 是正确的。我在代码方面进一步推进了一下:这里是一个在所有主要浏览器中都能正常工作的完整示例。我之前在 Stack Overflow 上发布过此代码的变体(例如这里这里),并使其变得通用,因此我(或其他人)不必对其进行太多更改即可重用。

jsFiddle 示例:http://jsfiddle.net/7Vf5J/38/

代码:

// Reusable generic function
function surroundInElement(el, regex, surrounderCreateFunc) {
    // script and style elements are left alone
    if (!/^(script|style)$/.test(el.tagName)) {
        var child = el.lastChild;
        while (child) {
            if (child.nodeType == 1) {
                surroundInElement(child, regex, surrounderCreateFunc);
            } else if (child.nodeType == 3) {
                surroundMatchingText(child, regex, surrounderCreateFunc);
            }
            child = child.previousSibling;
        }
    }
}

// Reusable generic function
function surroundMatchingText(textNode, regex, surrounderCreateFunc) {
    var parent = textNode.parentNode;
    var result, surroundingNode, matchedTextNode, matchLength, matchedText;
    while ( textNode && (result = regex.exec(textNode.data)) ) {
        matchedTextNode = textNode.splitText(result.index);
        matchedText = result[0];
        matchLength = matchedText.length;
        textNode = (matchedTextNode.length > matchLength) ?
            matchedTextNode.splitText(matchLength) : null;
        // Ensure searching starts at the beginning of the text node
        regex.lastIndex = 0;
        surroundingNode = surrounderCreateFunc(matchedTextNode.cloneNode(true));
        parent.insertBefore(surroundingNode, matchedTextNode);
        parent.removeChild(matchedTextNode);
    }
}

// This function does the surrounding for every matched piece of text
// and can be customized  to do what you like
function createSpan(matchedTextNode) {
    var el = document.createElement("span");
    el.style.color = "red";
    el.appendChild(matchedTextNode);
    return el;
}

// The main function
function wrapWords(container, words) {
    // Replace the words one at a time to ensure "test2" gets matched
    for (var i = 0, len = words.length; i < len; ++i) {
        surroundInElement(container, new RegExp(words[i]), createSpan);
    }
}

wrapWords(document.getElementById("container"), ["test2", "test"]);

1
@MikeMellor:将 new RegExp(words[i], "g") 改为 new RegExp(words[i], "gi") - Tim Down
@TimDown:感谢您的代码。但是需要注意的是,它有一个bug:会跳过一些匹配项。为了修复它,在surroundMatchingText中的textNode = ...行后面必须添加regex.lastIndex = 0; - dmitrych
@dchervov:经过仔细检查,我认为更改正则表达式以不设置其全局标志是更好的修复方法,并且这也是我最初打算让surroundMatchingText()函数工作的方式。 - Tim Down
@dchervov:尽管你的修复使其能够在正则表达式的全局标志设置与否的情况下工作,但我开始认同你的修复方案了。 - Tim Down
@TimDown:好的,谢谢。 - dmitrych
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接