2

I'm looking to replace all characters that appear in a webpage with another character, for example replace 'a' with 'A'. Except for one caveat which I will further explain, I currently have this working well with the following code:

function replaceTextOnPage(){
    getAllTextNodes().forEach(function(node){
      let map = new Map()
      map.set('さ', ['さ', 'サ'])
      node.nodeValue = node.nodeValue.replace(new RegExp(quote(map.get(node.)[0]), 'g'), map.get('さ')[1]);
    });
  
    function getAllTextNodes(){
      var result = [];
  
      (function scanSubTree(node){
        if(node.childNodes.length) 
          for(var i = 0; i < node.childNodes.length; i++) 
            scanSubTree(node.childNodes[i]);
        else if(node.nodeType == Node.TEXT_NODE) 
          result.push(node);
      })(document);
  
      return result;
    }
  
    function quote(str){
      return (str+'').replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
    }
  }

Now if we take a look at the upper portion, the second function

getAllTextNodes().forEach(function(node){
      let map = new Map()
      map.set('a', ['a', 'A'])
      node.nodeValue = node.nodeValue.replace(new RegExp(quote(map.get('a')[0]), 'g'), map.get('a')[1]);
    });

I use a map (for efficiency purposes if using this for replacements of many different characters). The way the code is written here works as I want - effectively replaces all 'a' with 'A'. map.get('a')[0] gets the value associated with 'a', which is an array at the 0 index, or 'a'. This is the value to be replaced. map.get('a')[1] then gets the value at the 1 index of the array, or 'A'. The value to replace with.

My question is making this process "generic" so to speak. Ultimately I will be swapping all values of around 60 different characters (for Japanese, but can be thought of as me swapping every distinct lower case with upper-case and vice-versa). I was thinking, as each character (current key) is being traversed over, that key's respective map value would replace the key. Doing this iteratively for each of the getAllTextNodes with O(1) map lookup time.

I essentially need a way to call the current whatever character is being currently iterated over. I have tried map.get(node.nodeValue) as well as map.get(node.textContent), but neither worked.

Any help is greatly appreciated!!

sonder
  • 49
  • 5
  • First, if you're trying to maximize efficiency, there's no need to repeatedly recreate `map` with each `forEach()` iteration. Just create `map` once, before the `forEach()` call. As for the map itself, why is each value an array when it just needs to be the associated "replace with" character? Last, if you want to be able to not specify `map.get('a')`, but still be able to replace many chars (not just one), you need to [forEach() over `map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach). – kmoser Jul 26 '20 at 02:21
  • Ah yes, I didn't even notice to look at the constant recreations of map. Was totally focused on having a prototype work with one character. I have the map value as an array because with Javascript, I couldn't find a map.getKey esque function. In any case I need to get the key value (key value is to be replaced). Using an array to get the index 0 value identical to the key was my work around. Maybe there's a better way. Maybe I'm misunderstanding the application of forEach over map but I don't think that will accomplish what I'm after. I will further elaborate in another comment – sonder Jul 26 '20 at 02:48
  • Ultimately I will be swapping all values of around 60 different characters (for Japanese, but can be thought of as me swapping every distinct lower case with upper-case and vice-versa). I was thinking, as each character (current key) is being traversed over, that key's respective map value would replace the key. Doing this iteratively for each of the getAllTextNodes with O(1) map lookup time. Is this implementable with forEach() over map as you described and I'm simply failing to see how? – sonder Jul 26 '20 at 02:48
  • 1
    My initial thought is that the only way to do this is pretty much what you're already doing: outer loop iterates through every text node, and inner loop iterates through all the replacements. Assuming you'll be doing more text replacements than iterations, the only room for optimization I can see is how you do the string replacement. Rather than iterating through all strings to be replaced, you could use a regexp containing multiple strings and replacements. This method, and other optimizations are [explained in this StackOverflow answer](https://stackoverflow.com/a/15604206/378779). – kmoser Jul 26 '20 at 03:57
  • This ended up working fantastically, thank you! One question if you happen to know; is there a way to make this run whenever new content is loaded (and only on the newly loaded content? Currently it replaces everything on the initial screen, but if I scroll to load more content there is no effect. – sonder Jul 26 '20 at 13:11
  • 1
    It sounds like you want to use a [mutation observer](https://stackoverflow.com/questions/7434685/how-can-i-be-notified-when-an-element-is-added-to-the-page/18257263#18257263). – kmoser Jul 26 '20 at 20:03

1 Answers1

1

If you are willing to use jQuery, you could use this stack overflow post here to get all of the textNodes on the page and then manipulate the text using the .map function.

Next, you could use an object as a dictionary to declare each change you want to take place.

For example:

var dict = {"a":"A", "b":"B"}

I have added some code to illustrate what I mean :)

This may not be the most performant strategy if you have many translations or individual blocks of text that have to take place because it is looping through every item in the dictionary for each block of text but it does work and is very easy to add more items to the dictionary if need be.

 jQuery.fn.textNodes = function() {
  return this.contents().filter(function() {
    return (this.nodeType === Node.TEXT_NODE && this.nodeValue.trim() !== "");
  });
}

var dict = {"a":"A", "b":"B", "t":"T"};

function changeToUppercase()
{
  //Manipulate the output how you see fit.
  $('*').textNodes().toArray().map(obj => obj.replaceWith(manipulateText($(obj).text())));
}

function manipulateText(inputText)
{
    var keys = Object.keys(dict);
    var returnText = inputText;

    for(var i = 0; i < keys.length; i++)
        {
        var key = keys[i];
            var value = dict[key];
    
            returnText = returnText.replace(key, value);
    }

    return returnText;
}

changeToUppercase();
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<p>test 1</p>
<p>test 2</p>
<p>test 3</p>
<p>test 4</p>
Zach
  • 31
  • 4
  • Unfortunately I intend to make a whole lot of replacements - essentially replacing every character on the webpage with its counterpart (Japanese Hiragana to Katakana). Performance gets slow pretty quickly. Thank you for your answer though – sonder Jul 26 '20 at 03:25