3

For an easter egg, I want to be able to replace every word on my webpage with a constant string. Existing questions focus on replacing certain words, elements that don't contain many other elements, or use libraries such as JQuery. Structure should be kept, i.e. <li>Some text <span>link</span> more text</li> should become <li>Lorem Lorem <span>Lorem</span> Lorem Lorem</li>.

If I walk the DOM like this:

recur (el) {
  if (typeof el === 'undefined') {
    el = document.body
  }
  for (let e of el.childNodes) {
    if (e.nodeType === 1) {
      recur(e)
    } else if (e.nodeType === 3) {
      console.log(e)
    }
  }
}

I get lots of #text elements and things that look like strings. How can I actually mutate them? I tried assigning to the corresponding elements in the parent array, but that doesn't work:

Uncaught TypeError: Failed to set an indexed property on 'NodeList': Index property setter is not supported.

Is it better to modify the innerHTML attribute on parent nodes of such text nodes?

Or would you recommend a different approach altogether? CSS-only solutions are also great.

5
  • If it's the same replacement string over and over, you could just count the number of words in an element and then set the nodes innerHTML with that many of your new word. Commented Jan 17, 2018 at 22:19
  • 1
    Possible duplicate of Change textNode value Commented Jan 17, 2018 at 22:21
  • Chris: You mean with innerHTML? Yes, but what are the relevant elements? What if I have nesting à la <li>Some text <span>link</span> more text</li> Commented Jan 17, 2018 at 22:22
  • Can you provide us a sample input, and a sample output? Commented Jan 17, 2018 at 22:22
  • I added one in the first paragraph of the question. Commented Jan 17, 2018 at 22:25

3 Answers 3

5

How fun! You can get and assign to the nodeValue of a text node. regex will get you at the very least really close with /[\w\x7f-\xff]+/g (The hex values match a good portion of special characters. See here)

(function recur (el) {
  if (typeof el === 'undefined') {
    el = document.body
  }
  for (let e of el.childNodes) {
    if (e.nodeType === Node.ELEMENT_NODE) {
      recur(e)
    } else if (e.nodeType === Node.TEXT_NODE) {
      e.nodeValue = e.nodeValue.replace(/[\w\x7f-\xff]+/g, 'lorem')
    }
  }
})();
<html>
<head>
<title>
A Simple HTML Document
</title>
</head>
<body>
<p>This is a very schön HTML document</p>
<p>It only has <b>two</b> paragraphs</p>
</body>
</html>

Sign up to request clarification or add additional context in comments.

4 Comments

minor suggestion: using the Node.ELEMENT_NODE and Node.TEXT_NODE constants can make it easier to understand how this works.
Great! That solution works, I just need to fiddle with the regex a bit to include umlauts and such.
@Semicolon A very good suggestion. For those wondering, Semicolon is referring to these constants: developer.mozilla.org/en-US/docs/Web/API/Node/…
Alternatively, you can use something like const n = e.nodeValue.split(' ').filter(str => str.trim()).length; e.nodeValue = 'Lorem '.repeat(n)
2

I would highly suggest you look into using a TreeWalker, a DOM API used for exactly this purpose - traversing a DOM tree. It can find all of the text nodes in the document and it finds them very efficiently, since modern browsers run API calls like this in native code.

The first argument is the node you want to begin the search from and the second argument is the type of nodes to find. In this case, we're starting at the document.body and looking for all textNodes. Then, in a while loop, you perform whatever action you want on the node and then move on to the next one.

With this implantation, you don't have to deal with awkward inner tags or white space - all of the text gets bundled up nicely for you. And, it will be faster than anything you can write in client-side JS for tree traversal.

const matchAllExceptWhitespace = /\w+/g;
const treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
const word = 'Lorem';

let node;

while (node = treeWalker.nextNode()) {
  node.textContent = node.textContent.replace(matchAllExceptWhitespace, word);
}
<h1>A paragraph</h1>
<p>This is a sentence.</p>
<ul>
    <li>Some text <span>link</span> more text</li>.
</ul>

2 Comments

Wouldn't it be easier to do: let node; while (node = treeWalker.nextNode()) { node.textContent = node.textContent.replace(matchAllExceptWhitespace, word); }?
Yes, thanks, that makes it simpler, I've updated the snippet. There's also a reference to the currentNode() which could be used.
1

This is one of the simplest ways (Borrowed some help from this answer):

function ReplaceChildText(node, replaceText) {
    //Is this a text node?
    if (node.nodeType === 3) {
      //Simply replace the value with your regex:
      node.nodeValue = node.nodeValue.replace(/\w+/g, replaceText);
    } else {
        //Loop through all the children, to look for more text nodes
        for (let child of node.childNodes) {
            //Yay, recursion
            ReplaceChildText(child, replaceText);
        }
    }
}

ReplaceChildText(document.body, 'Loreum');
<div id="test"><li>Some text <b>link</b> more text</li></div>

3 Comments

There are other node types that don’t have childNodes which could be encountered, like Comment, so a check for whether it’s an Element (or whether it has childNodes) matters.
@Semicolon Just added a comment node, and the script still executed perfectly (childNodes simply returned 0 length)
Oh, good to know. I hadn’t realized that childNodes is inherited from Node.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.