1

I am trying to find the right JavaScript code for a browser extension that replaces the word "winter" with the word "summer" in the text of a web page.

First attempt

Based on this blog post, my first attempt was

function wordFilter() {

    let target = "winter";
    let replace = "summer";

    var html = document.querySelector('html');
    var walker = document.createTreeWalker(html, NodeFilter.SHOW_TEXT);
    var node;
    while (node = walker.nextNode()) {
        node.nodeValue = node.nodeValue.replace(target, replace);
    }
}

wordFilter()

This almost works. It does replace "winter" with "summer", but the word "winter" still appears onscreen for a few seconds before it's replaced. I want the filter to work from the very beginning.

At the very least, this attempt shows that my general extension setup an permissions are correct, so any further failures must be code problems.

Second attempt

My second attempt was based on this answer to a StackOverflow post. It's an involved post, but the section under "Naive enumeration of added nodes" is closest to what I want. The code in the onMutation() function searches for <h1> elements that contain the target text; just for proof-of-concept, I simplified that part of the code to apply to all Elements that are descendants of an added Element. If the initial DOM load qualifies as a set of Mutations (which I'm still not clear on, but from context that appears to be the expected behavior), then this should capture every Element and every piece of text on the page.

The code:

var observer = new MutationObserver(onMutation);
observer.observe(document, {
    childList: true, // report added/removed nodes
    subtree: true,   // observe any descendant elements
});

function onMutation(mutations, observer) {
    for (var i = 0, len = mutations.length; i < len; i++) {
        var added = mutations[i].addedNodes;
        for (var j = 0, node; (node = added[j]); j++) {
            replaceText(node);  
        }
    }
}

function replaceText(el) {
    let target_string = "winter";
    let replacement_string = "summer";

    const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
    for (let node; (node = walker.nextNode());) {
        const text = node.nodeValue;
        const newText = text.replace(target_string, replacement_string);
        if (text !== newText) {
            node.nodeValue = newText;
        }
    }
}

This does not work at all. No instance of the word "winter" is replaced with anything at any time under any conditions.

Now I'm at a loss. How do I replace text on a web page without the original text appearing before replacement?

Nimantha
  • 6,405
  • 6
  • 28
  • 69
Darien Marks
  • 446
  • 4
  • 18
  • How and when do you inject your script? If it's in the manifest, what is `run_at` set to? – Jason Goemaat Nov 12 '21 at 14:05
  • @JasonGoemaat `run_at` is not in the manifest. I inject the script by the standard Firefox procedure outlined [here](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension) using "Load Temporary Add-on" in `about:debugging`. – Darien Marks Nov 12 '21 at 14:26
  • I think you should add [`run_at`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/content_scripts) to your script definition in the manifest and set it to `document_end` (DOM has finished loading, but resources such as scripts and images may still be loading). The default is `document_idle` which waits to execute your script until images have finished loading and other scripts have finished loading and executing. – Jason Goemaat Nov 12 '21 at 14:56
  • @JasonGoemaat That's a good thought, but it didn't work. I added `"run_at": "document_end"` to the `content_scripts` object and re-tried both approaches. The first still showed "winter" before replacing it, and the second still didn't work at all. – Darien Marks Nov 12 '21 at 15:25
  • @JasonGoemaat *However*, `"run_at": "document_start"` combined with the MutationObserver approach does seem to work! I guess running at `document_end` or `document_idle` means we don't start looking for mutations until after the document's initial load, which is why it didn't work. Do you want to write it up as an answer, or should I? – Darien Marks Nov 12 '21 at 15:35
  • Go ahead and answer – Jason Goemaat Nov 12 '21 at 15:55

1 Answers1

0

I can confirm that wOxxOm's selection of approaches, linked in my post, work well, including the one I attempted to use.

The issue in this case is the timing of when the browser inserts the code.

manifest.json allows a "content_scripts" key "run_at" to specify when the browser should load content scripts (more at MDN). The default is "document_idle", which seems to mean "whenever you get the chance."

With this default, the browser was loading the page, then waiting until it had an idle moment to load the extension script to start looking for Mutations. Thus, the MutationObserver missed all of the initial mutations, and the script appeared not to work. (As an aside: I'm now confident that the initial loading of each DOM Element does count as a Mutation to the DOM).

By specifying "run_at": "document_start" in manifest.json like so:

"content_scripts": [
    {
        "js": ["filename.js"],
        "run_at": "document_start"
    }
]

the browser runs the script to watch for Mutations as soon as it starts to load the DOM, so the initial Mutations are caught, and the text is changed before loading so that the original "winter" never appears on-page before being changed to "summer".

Special thanks to @Jason Goemaat for the hint.

Darien Marks
  • 446
  • 4
  • 18