Replacing multiple strings of text on webpage using javascript

Viewed 482

I want to make a google chrome extension that replaces many different strings of text on a webpage to exhibit different words on client side. I came accros with this example below.

But I failed after trying a lot to change it to handle different words to replace. I can only handle one at a time.

i.e: I want to change all 'pink' words to 'blue', all 'cat' words to 'dog' and all 'boy' words to 'girl'. All at once.

How could I accomplish that? When I tinkered with this sample code all the times I would end only changing one of the words. In some cases, only on its first occurence.

Thanks in advance. I could not an answer to this question anywhere. Sorry if it looks noobish.

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 replacedText = text.replace(/pink/gi, 'blue');

            if (replacedText !== text) {
                element.replaceChild(document.createTextNode(replacedText), node);
            }
        }
    }
}

4 Answers

In order to simplify mapping of multiple strings to their replacements, you could create a js object and corresponding function for your replacements.

Edited for improved usability per comment from @MihaiIorga (now you only have to edit matches to add new replacement words). For example (only selecting <div> tags in the snippet below for simplicity but you can modify to select all elements, certain tags, etc):

const matches = { pink: 'blue', cat: 'dog', boy: 'girl' };
const replaces = Object.keys(matches).join('|');
const replacer = (s) => matches[s];
const elems = document.getElementsByTagName('div');
for (const elem of elems) {
  const s = elem.textContent;
  elem.textContent = s.replace(new RegExp(replaces, 'gi'), replacer);
}
<div>pink horse</div>
<div>green cat</div>
<div>purple boy moon</div>
<div>boy pink cat</div>

The wildcard is "too wild" I think you will need to use '*:not(script):not(link) ... '

Or maybe something like: var elements = document.getElementsByTagName('body')[0].querySelectorAll('*');

var elements = document.getElementsByTagName('body')[0].querySelectorAll('*:not(script)');
[].forEach.call(elements,function(elem){
    elem.textContent = elem.textContent ?  elem.textContent.replace(/pink/igm,'blue') : elem.innerText;
});

There are a few key things to note here:

  1. You're already grabbing every element on the page with your wildcard getElementsByTagName search. Therefore, you don't have to traverse the tree down to the child nodes, as you've already captured them in the initial search and they'll come up later in the iterations.

  2. You don't have to replace a string, compare it to the original contents, then replace the node. You can just directly replace the inner text of any node.

  3. Square brackets in regular expressions indicate "classes" of characters to match. That means, without special characters, it will match any character inside the brackets, not all the characters, and order doesn't matter. In other words, /[one]/ will match o, n, or e, it won't match the string "one". So no square brackets needed.

So (without adding methods with callbacks -- though you should look into Array.forEach at least, as it's useful!), your code simplifies to just:

elements = document.getElementsByTagName("*");
for (var i = 0; i < elements.length; i++) {
    elements[i].innerText = elements[i].innerText.replace(/pink/gi, "blue");
}

So there is a very similar question posted here, though it completely replaces the text content of each child node, rather than replacing selectively. I've modified it, here's a possible way to streamline it. First, use a function, so you're hiding away some of the complexity. ;)

let doc = document.querySelector("article"),
    pinkBtn = document.querySelector("#pink-btn"),
    pinkBoyBtn=document.querySelector("#pink-boy-btn"),
    pinkBoyDogBtn=document.querySelector("#pink-boy-dog-btn");


pinkBtn.addEventListener("click", function(){
  replaceTextNodes(doc, /pink/gi, "blue");
});
pinkBoyBtn.addEventListener("click", function(){
  replaceTextNodes(doc, /pink/gi, "blue");
  replaceTextNodes(doc, /boy/gi, "girl");
})
pinkBoyDogBtn.addEventListener("click", function(){
  replaceTextNodes(doc, /pink/gi, "blue");
  replaceTextNodes(doc, /boy/gi, "girl");
  replaceTextNodes(doc, /dog/gi, "cat");
})

function replaceTextNodes(node, oldText, newText) {
  node.childNodes.forEach(function(el) {
    if (el.nodeType === 3) {  // If this is a text node, replace the text
      if (el.nodeValue.trim() !== "") { // Ignore this node it it an empty text node
        el.nodeValue = el.nodeValue.replace(oldText, newText);
      }
    } else { // Else recurse on this node
      replaceTextNodes(el, oldText, newText);
    }
  });
}
article {
 border: 1px solid blue;
 padding: 2px;
}
section{
 border: 1px dotted red;
 padding: 2px;
}
<article>
      <h1>This is a pink page</h1>

      <section id="home">
        Pink pink dog cat cow chicken boy
      </section>
      <section id="about">
        this is a story about a pink dog named Clyde.
      </section>
      <section id="projects">
        And more on the Clyde story. He has a boy.
      </section>
    </article>
    
    <button id="pink-btn">'pink'-&gt;'blue'</button> | <button id="pink-boy-btn">'pink'-&gt;'blue', 'boy'-&gt;'girl'</button> | <button id="pink-boy-dog-btn">'pink'-&gt;'blue', 'boy'-&gt;'girl', 'dog'-&gt;'cat'</button>

Thing to note, I tell it what to use as the root node, and to change all the text of the elements below that. I pass as parameters a DOM node, the old text, and the new text value. For old text, I use a regex.

This is tricky, as I'm using recursion (if the current child node isn't a text node, recall my function on that child node, using it as the parent). Confusing, but very efficient.

Hope this helps!

Related