Skip to content Skip to sidebar Skip to footer

Jquery Replace Matching Text With Html

I want to use jQuery to dynamically enclose all text matching a given regexp with a specific tag. For example:
Eggs an

Solution 1:

How can I match only text, but also be able to replace that text with HTML elements?

You could select all the descendant elements and text nodes and filter the set to only include text nodes. Then you can simply replace each text node with the modified string:

$(".content *").contents().filter(function() {
  returnthis.nodeType === 3;
}).replaceWith(function () {
  returnthis.textContent.replace(/(ham|spam)/g, '<span class="meat-product">$1</span>');
});

$(".content *").contents().filter(function() {
  returnthis.nodeType === 3;
}).replaceWith(function() {
  returnthis.textContent.replace(/(ham|spam)/g, '<span class="meat-product">$1</span>');
});
.meat-product {
  color: #f00;
}
<scriptsrc="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script><divclass="content"><divclass="spamcontainer">
    Eggs and spam is better than ham and spam.
    <imgsrc="spam-and-eggs.jpg">I do not like green eggs and spam, though.
  </div></div>
  • Use the universal selector, *, to select all descendant elements (of course you could also use $('.content').children(), but that will only go one level deep... you will probably encounter nested elements which means that you will need to retrieve all text nodes, therefore I used * to select the contents of all the descendant elements).
  • The .contents() method gets all the text nodes and children elements.
  • Then filter the set so that only text nodes are included since we don't want to replace any HTML. The nodeType property value of a text node is 3.
  • Finally, the .replaceWith() method is used to update each text node with the replaced text string. The good thing about the .replaceWith() method is that you can replace the text with an HTML string.
  • You can also shorten your .replace() method by using a capturing group like in the example above. Then you can simply substitute $1 between the span elements.

Solution 2:

Here's a solution that covers the trivial case shown and is only presented to show how to walk through text nodes with jQuery. It would require some recursive DOM walking if you have to deal with deeper nesting

var reg = /ham|spam/g;

$('.content').children().contents().each(function() {
    if (this.nodeType === 3) {
        var txt = this.textContent;
        if (txt.match(reg)) {
            $(this).replaceWith(textToHtml(this));
        }
    } 
});


functiontextToHtml(node) {
    return node.textContent.replace(reg, function(match) {
        return'<span class="meat-product">' + match + '</span>';
    })
}

As mentioned in comments there are battle tested plugins that will do this for you

DEMO

Solution 3:

I ended up using Josh Crozier's solution with a twist. The problem is that if there are & < > characters in the text, they need to be escaped if they are being used as the HTML argument to replaceWith(). And they can't be universally escaped if you are inserting HTML intentionally. So I did something like this:

functionescapeHTML(s) {
    return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
$("div.content *").contents().filter(function() {
    returnthis.nodeType === 3;
}).each(function() {
    var node = $(this);
    var text = node.text();
    var occurrences = 0;
    var html = text.replace(/(ham|spam)|.+?/g, function(match, p1) {                
            if (p1)
            {   
                // we found ham or spam
                ++occurrences;
                return'<span class="meat-product">'
                    + escapeHTML(match)    // technically not necessary // since we know "ham" and "spam"// don't need escaping, but some regex's// need this
                    + '</span>';
            }
            else
            {
                return escapeHTML(match);
            }
        });
    if (occurrences > 0)
    {
        node.replaceWith(html);
    }
});

Solution 4:

I appreciate that the OP specifically stated

I want to use jQuery

but I wanted to see if I could build a plain vanilla javascript solution. (I have yet to start learning jQuery).

In the end my solution involved 3 steps:

  1. Identify which elements have text nodes (which match the regex) as direct childNodes;
  2. Clone each of these elements and rebuild them from empty, using the original elements as templates;
  3. Remove each original element, leaving the newly built clones

/* ### Build an array of 'matching elements' which have (as direct childNodes) text nodes containing 'spam' or 'ham' */var matchingElements = [];
var allElements = document.querySelectorAll('*:not(script)');

for (var i = 0; i < allElements.length; i++) {
	var elementAdded = false;
    for (var j = 0; j < allElements[i].childNodes.length; j++) {
        if (allElements[i].childNodes[j].nodeValue === null) {continue;}
        if (allElements[i].childNodes[j].nodeValue.match(/ham|spam/gi) === null) {continue;}
        if (elementAdded !== true) {
        	matchingElements.push(allElements[i]);
        	elementAdded = true;
        }
    }
}

/* ### Clone each matched element and rebuild it, replacing unstructured text nodes */var x;
var y;
var newTextNode;
var clonedElements = [];
var meatProducts = document.getElementsByClassName('meat-product');

/* Create (empty) clone of matched element */for (var k = 0; k < matchingElements.length; k++) {
        clonedElements[k] = matchingElements[k].cloneNode(false);
        matchingElements[k].parentNode.insertBefore(clonedElements[k],matchingElements[k]);

    for (var l = 0; l < matchingElements[k].childNodes.length; l++) {

/* Add non-text nodes to cloned element */if (matchingElements[k].childNodes[l].nodeValue === null) {
            var clonedNode = matchingElements[k].childNodes[l].cloneNode(true);
            clonedElements[k].appendChild(clonedNode);
            continue;
        }

/* Add text nodes which don't contain 'spam' or 'ham' to cloned element */if (matchingElements[k].childNodes[l].nodeValue.match(/ham|spam/gi) === null) {
            var clonedNode = matchingElements[k].childNodes[l].cloneNode(true);
            clonedElements[k].appendChild(clonedNode);
            continue;
        }

/* Restructure and add text nodes which do contain 'spam' or 'ham' to cloned element */var meatLarder = matchingElements[k].childNodes[l].nodeValue.match(/ham|spam/gi);
        var textNodeString = matchingElements[k].childNodes[l].nodeValue;

        for (m = 0; m < meatLarder.length; m++) {
        x = 0;
        y = textNodeString.indexOf(meatLarder[m]);

        newTextNode = document.createTextNode(textNodeString.substring(x,y));
        clonedElements[k].appendChild(newTextNode);

        textNodeString = textNodeString.substring(y);

        y = meatLarder[m].length;

        var newSpan = document.createElement('span');
        var newMeatProduct = document.createTextNode(meatLarder[m]);
        var newClass = document.createAttribute('class');
        newClass.value = 'meat-product';
        newSpan.setAttributeNode(newClass);
        newSpan.appendChild(newMeatProduct);
        clonedElements[k].appendChild(newSpan);

        textNodeString = textNodeString.substring(y);
        }

        newTextNode = document.createTextNode(textNodeString);
        clonedElements[k].appendChild(newTextNode);
    }

/* Remove original matched element from document, leaving only the newly-built cloned element */
    matchingElements[k].parentElement.removeChild(matchingElements[k]);
}
<divclass="content"><divclass="spamcontainer">
Eggs and spam is better than ham and spam.
<imgsrc="spam-and-eggs.jpg">
I do not like green eggs and spam, though.
</div></div>

Post a Comment for "Jquery Replace Matching Text With Html"