使用 .replace() 突出显示页面上的文字

Highlight a word of text on the page using .replace()

我正在开发一个 Google Chrome 扩展程序,它允许您自动将突出显示 CSS 规则应用于您选择的单词。

我有以下代码

var elements = document.getElementsByTagName('*');

for (var i=0; i<elements.length; i++) {
    var element = elements[i];

    for (var j=0; j<element.childNodes.length; j++) {
        var node = element.childNodes[j];

        if(node.nodeType === 3) {
            var text = node.nodeValue;

            var fetchedText = text.match(/teste/gi);

            if(fetchedText) {
                var replacedText = element.innerHTML.replace(/(teste)/gi, "<span style=\"background-color: yellow\"></span>");

                if (replacedText !== text) {
                    element.innerHTML = replacedText;
                }
            }
        }
    }
}

这会破坏并冻结我的 Chrome 标签页。但是,如果我从 element.innerHTML = replacedText; 切换到 element.innerHTML = "text"; 这有效。

我似乎找不到以下代码有什么问题。

您首先测试 #text 节点以查看文本是否包含您要突出显示的单词,然后对父元素的 .innerHTML 执行替换。这有几个问题。

  • 无限替换:当您修改父元素的 .innerHTML 时,您会更改 childNodes 数组。您这样做的方式是在包含要替换的文本的数组中进一步添加一个节点。因此,当您继续扫描 childNodes 数组时,您总是会找到一个包含您要替换的文本的(新)节点。因此,您再次替换它,创建另一个在 childNodes 数组中具有更高索引的节点。无限重复。
  • 使用 RegExp 替换 .innerHTML 属性 中的文本。虽然您已经进行了测试以确保要替换的文本确实包含在文本节点中,但这并不能阻止您的 RegExp also 替换实际 [=96] 中的任何匹配词=] 元素(例如在 src="yourWord"href="http://foo.com/yourWord/bar.html" 中,或者如果试图突出显示 stylecolorbackgroundspan , id, height, width, button, form, input, 等等)。
  • 您没有检查以确保您没有更改 <script><style> 标签中的文本。
  • 您正在检查您是否仅在文本节点中进行更改(即您检查 node.nodeType === 3)。如果您不检查这一点,由于使用 .innerHTML 更改 HTML,您还会遇到以下可能的问题:
    • 您最终可能会更改属性或实际的 HTML 标签,具体取决于您使用 .replace() 更改的内容。这可能会完全破坏页面布局和功能。
    • 当您更改 .innerHTML 时,该页面部分的 DOM 将完全重新创建。这意味着元素,虽然新元素可能是具有相同属性的相同类型,但附加到旧元素的任何事件侦听器都不会附加到新元素。这会严重破坏页面的功能。
    • 重复更改 DOM 的大部分内容可能需要大量计算才能重新呈现页面。根据您执行此操作的方式,您可能 运行 遇到用户认为的重大性能问题。

因此,如果要使用正则表达式替换文本,只需对 #text 节点的内容执行操作,而不需要对父节点的 .innerHTML 执行操作节点。因为您想要创建额外的 HTML 元素(例如新的 <span style=""> 元素,带有子 #text 节点),所以会有些复杂。

无法将 HTML 文本分配给文本节点以创建新的 HTML 节点:

无法将新 HTML 直接分配给文本节点并将其计算为 HTML,从而创建新节点。分配给文本节点的 .innerHTML 属性 将在对象上创建这样的 属性 (就像在任何对象上一样),但不会更改屏幕上显示的文本(即#text 节点的实际值)。因此,它不会完成您想要做的事情:它不会创建任何新的 HTML 父节点的子节点。

对页面 DOM 影响最小(即最不可能破坏页面上现有的 JavaScript)的方法是创建一个 <span> 以包含您正在创建的新文本节点(#text 节点中不在彩色 <span> 中的文本)以及您正在创建的潜在多个 <span> 元素。这将导致用单个 <span> 元素替换单个 #text 节点。虽然这会创建额外的后代,但它会使父元素中的子元素数量保持不变。因此,任何依赖它的 JavaScript 都不会受到影响。鉴于我们正在更改 DOM,没有办法不潜在地破坏其他 JavaScript,但这应该最大限度地减少这种可能性。

如何执行此操作的一些示例:请参阅其他两个示例中使用的 (replaces a list of words with those words in buttons) and (places all text in <p> elements which is separated by spaces into buttons) for full extensions that perform regex replace with new HTML. See which does basically the same thing, but makes a link (it has a different implementation which traverses the DOM with a TreeWalker to find #text nodes instead of a NodeIterator

这里的代码将在 document.body 中的每个文本节点上执行您想要的替换,并创建新的 HTML 以使 style 在部分文字:

function handleTextNode(textNode) {
    if(textNode.nodeName !== '#text'
        || textNode.parentNode.nodeName === 'SCRIPT' 
        || textNode.parentNode.nodeName === 'STYLE'
    ) {
        //Don't do anything except on text nodes, which are not children 
        //  of <script> or <style>.
        return;
    }
    let origText = textNode.textContent;
    let newHtml=origText.replace(/(teste)/gi
                                 ,'<span style="background-color: yellow"></span>');
    //Only change the DOM if we actually made a replacement in the text.
    //Compare the strings, as it should be faster than a second RegExp operation and
    //  lets us use the RegExp in only one place for maintainability.
    if( newHtml !== origText) {
        let newSpan = document.createElement('span');
        newSpan.innerHTML = newHtml;
        textNode.parentNode.replaceChild(newSpan,textNode);
    }
}

let textNodes = [];
//Create a NodeIterator to get the text nodes in the body of the document
let nodeIter = document.createNodeIterator(document.body,NodeFilter.SHOW_TEXT);
let currentNode;
//Add the text nodes found to the list of text nodes to process.
while(currentNode = nodeIter.nextNode()) {
    textNodes.push(currentNode);
}
//Process each text node
textNodes.forEach(function(el){
    handleTextNode(el);
});

还有其他方法可以做到这一点。但是,它们会对该特定元素的子元素结构产生更重大的更改(例如,父元素上的多个附加节点)。这样做更有可能破坏页面上已经依赖于页面当前结构的任何 JavaScript。实际上,像这样的任何变化都有可能打破电流 JavaScript。

此答案中的代码是根据

中的代码修改的

我遇到的错误是由于递归循环,因为例如,我正在寻找关键字 teste 并且我正在插入一个内容为 <span style=\"background-color: #ffff00\">teste</span> 的新元素,这将强制尝试再次替换新关键字 teste 的脚本,依此类推。

我想出了这个功能:

function applyReplacementRule(node) {
    // Ignore any node whose tag is banned
    if (!node || $.inArray(node.tagName, hwBannedTags) !== -1) { return; }

    try {
        $(node).contents().each(function (i, v) {
            // Ignore any child node that has been replaced already or doesn't contain text
            if (v.isReplaced || v.nodeType !== Node.TEXT_NODE) { return; }

            // Apply each replacement in order
            hwReplacements.then(function (replacements) {
                replacements.words.forEach(function (replacement) {
                    //if( !replacement.active ) return;
                    var matchedText = v.textContent.match(new RegExp(replacement, "i"));

                    if (matchedText) {
                        // Use `` instead of '' or "" if you want to use ${variable} inside a string
                        // For more information visit https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
                        var replacedText = node.innerHTML.replace(new RegExp(`(${replacement})`, "i"), "<span style=\"background-color: #ffff00\"></span>");

                        node.innerHTML = replacedText;
                    }
                });
            }).catch(function (reason) {
                console.log("Handle rejected promise (" + reason + ") here.");
            });

            v.isReplaced = true;
        });
    } catch (err) {
        // Basically this means that an iframe had a cross-domain source
        if (err.name !== "SecurityError")
        { throw err; }
    }
}

在我修改节点 属性 和 "tell" 的地方,我已经修改了那个节点,所以我不会再次陷入递归无限循环。

P.S。如您所见,此解决方案使用 jQuery。我将尝试重写它以仅使用 Vanilla JS。