Insert HTML into text node with JavaScript

The following article gives you the code to replace text with HTML elements:

http://blog.alexanderdickson.com/javascript-replacing-text

From the article:

var matchText = function(node, regex, callback, excludeElements) { 

    excludeElements || (excludeElements = ['script', 'style', 'iframe', 'canvas']);
    var child = node.firstChild;

    do {
        switch (child.nodeType) {
        case 1:
            if (excludeElements.indexOf(child.tagName.toLowerCase()) > -1) {
                continue;
            }
            matchText(child, regex, callback, excludeElements);
            break;
        case 3:
           child.data.replace(regex, function(all) {
                var args = [].slice.call(arguments),
                    offset = args[args.length - 2],
                    newTextNode = child.splitText(offset);

                newTextNode.data = newTextNode.data.substr(all.length);
                callback.apply(window, [child].concat(args));
                child = newTextNode;
            });
            break;
        }
    } while (child = child.nextSibling);

    return node;
}

Usage:

matchText(document.getElementsByTagName("article")[0], new RegExp("\\b" + searchTerm + "\\b", "g"), function(node, match, offset) {
    var span = document.createElement("span");
    span.className = "search-term";
    span.textContent = match;
    node.parentNode.insertBefore(span, node.nextSibling); 
});

And the explanation:

Essentially, the right way to do it is…

  1. Iterate over all text nodes.
  2. Find the substring in text nodes.
  3. Split it at the offset.
  4. Insert a span element in between the split.

You may need node to be the parent node, that way you can just use innerHTML:

node.innerHTML=node.childNodes[0].nodeValue.replace(/lol/, "<span>lol</span>");

Here node.childNodes[0] refers to the actual text node, and node is its containing element.


Not saying this is a better answer, but I'm posting what I did for completeness. In my case I have already looked up or determined the offsets of the text that I needed to highlight in a particular #text node. This also clarifies the steps.

//node is a #text node, startIndex is the beginning location of the text to highlight, and endIndex is the index of the character just after the text to highlight     

var parentNode = node.parentNode;

// break the node text into 3 parts: part1 - before the selected text, part2- the text to highlight, and part3 - the text after the highlight
var s = node.nodeValue;

// get the text before the highlight
var part1 = s.substring(0, startIndex);

// get the text that will be highlighted
var part2 = s.substring(startIndex, endIndex);

// get the part after the highlight
var part3 = s.substring(endIndex);

// replace the text node with the new nodes
var textNode = document.createTextNode(part1);
parentNode.replaceChild(textNode, node);

// create a span node and add it to the parent immediately after the first text node
var spanNode = document.createElement("span");
spanNode.className = "HighlightedText";
parentNode.insertBefore(spanNode, textNode.nextSibling);

// create a text node for the highlighted text and add it to the span node
textNode = document.createTextNode(part2);
spanNode.appendChild(textNode);

// create a text node for the text after the highlight and add it after the span node
textNode = document.createTextNode(part3);
parentNode.insertBefore(textNode, spanNode.nextSibling);

The answer presented by Andreas Josas is quite good. However the code had several bugs when the search term appeared several times in the same text node. Here is the solution with those bugs fixed and additionally the insert is factored up into matchText for easier use and understanding. Now only the new tag is constructed in the callback and passed back to matchText by a return.

Updated matchText function with bug fixes:

var matchText = function(node, regex, callback, excludeElements) { 

    excludeElements || (excludeElements = ['script', 'style', 'iframe', 'canvas']);
    var child = node.firstChild;

    while (child) {
        switch (child.nodeType) {
        case 1:
            if (excludeElements.indexOf(child.tagName.toLowerCase()) > -1)
                break;
            matchText(child, regex, callback, excludeElements);
            break;
        case 3:
            var bk = 0;
            child.data.replace(regex, function(all) {
                var args = [].slice.call(arguments),
                    offset = args[args.length - 2],
                    newTextNode = child.splitText(offset+bk), tag;
                bk -= child.data.length + all.length;

                newTextNode.data = newTextNode.data.substr(all.length);
                tag = callback.apply(window, [child].concat(args));
                child.parentNode.insertBefore(tag, newTextNode);
                child = newTextNode;
            });
            regex.lastIndex = 0;
            break;
        }

        child = child.nextSibling;
    }

    return node;
};

Usage:

matchText(document.getElementsByTagName("article")[0], new RegExp("\\b" + searchTerm + "\\b", "g"), function(node, match, offset) {
    var span = document.createElement("span");
    span.className = "search-term";
    span.textContent = match;
    return span;
});

If you desire to insert anchor (link) tags instead of span tags, change the create element to be "a" instead of "span", add a line to add the href attribute to the tag, and add 'a' to the excludeElements list so that links will not be created inside links.