Search code examples
javascripthtmlcssdomnodes

How to wrap DOM keywords in a span tag


I am writing a JavaScript script that will walk the DOM and wrap a specific keyword in a <span> tag. I want my script to wrap any occurrences of the word pan in a <span> so I can style it using <span style='color: red'. I don't actually want to use the word, pan, I am just using it as an example.

I have already reviewed many similar posts here, but none of them solve my problem. Most are either nuclear, over-complicated and confusing, or over-simplified and do not work as I've intended.

Here is what I have written so far:

<html>
  <body>
    <p>My <span style='font-weight: bold'>favorite</span> kitchen item is the pan.</p>
    <p>A pan can also be used as a weapon.</p>
    <script>
      // walk the document body
      function walk (node) {
        // if text node
        if (node.nodeType == 3) {
          // will always be an element
          const parent = node.parentNode;
          
          // ignore script and style tags
          const tagName = parent.tagName;
          if (tagName !== 'SCRIPT' && tagName !== 'STYLE') {
            
            // wrap occurrences of 'pan' in a red `<span>` tag
            const span = '<span style="color: red">pan</span>';
            parent.innerHTML = parent.innerHTML.replace (/pan/g, span)
          }
        }
        node = node.firstChild;
        while (node) {
          walk (node);
          node = node.nextSibling;
        }
      }
      walk (document.body)
    </script>
  </body>
</html>

This code runs as intended most of the time. However, in this example, it doesn't. If you were to run this code, this would be the result.

I know what is causing this. However, I have no idea how to resolve it.

Two of the text nodes, My and kitchen item is the pan. have a parent element with the following innerHTML: My <span style="font-weight: bold">favorite</span> kitchen item is the pan. The "pan" in <span> is being replaced, and is causing the problem.

If I use parentNode.textContent instead of parentNode.innerHTML, it does not wrap it in a <span> tag, it inserts it as visible text.

I understand this could be fixed by changing /pan/g to /\bpan\b/g, but that only fixes this example I created. I need the <span> tag to be only inserted into text content, and not tag names or other HTML.

What should I do?


Solution

  • Search a given htmlString with an escaped search string. Doing so (with appropriate escaping) will help avoid problems like matching HTML tags (ex. <span>) or substrings (ex. Pandora).

    /*
    highlight(selector, string)
    @ Params:
      selector [String]: Same syntax as CSS/jQuery selector
      string   [String]: Seach string
    */
    // A: Get the htmlString of the target's content
    // B: Escape the search string
    // C: Create a RegExp Object of the escaped search string
    // D: Find and replace all matches with match wrapped in a <mark>
    // E: Remove original HTML
    // F: Insert new HTML
    function highlight(selector, string) {
      let dom = document.querySelector(selector);
      let str = dom.innerHTML; //A
      let esc = `(?!(?:[^<]+>|[^>]+<\\/a>))\\b(${string})\\b`; //B
      let rgx = new RegExp(esc, "gi"); //C
      let txt = str.replace(rgx, `<mark>$1</mark>`); //D
      dom.replaceChildren(); //E 
      dom.insertAdjacentHTML('beforeend', txt); //F
    }
    
    highlight('body', "pan");
    <html>
    
    <body>
      <p>My <span style='font-weight: bold'>favorite</span> kitchen item is the pan.</p>
      <p>A pan can also be used as a weapon.</p>
      <p>Pan was the Greek god of the wild.</p>
      <p>Eboli was breifly a pandemic threat.</p>
    
    </body>
    
    </html>