Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Replace text in the middle of a TextNode with an element

I want to insert html tags within a text node with TreeWalker, but TreeWalker forces my html brackets into & lt; & gt; no matter what I've tried. Here is the code:

var text;
var tree = document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT);
while (tree.nextNode()) {
    text = tree.currentNode.nodeValue;
    text = text.replace(/(\W)(\w+)/g, '$1<element onmouseover="sendWord(\'$2\')">$2</element>');
    text = text.replace(/^(\w+)/, '<element onmouseover="sendWord(\'$1\')">$1</element>');
    tree.currentNode.nodeValue = text;
}

Using \< or " instead of ' won't help. My workaround is to copy all of the DOM tree to a string and to replace the html body with that. It works on very simple webpages and solves my first problem, but is a bad hack and won't work on anything more than a trivial page. I was wondering if I could just work straight with the text node rather than use a workaround. Here is the code for the (currently buggy) workaround:

var text;
var newHTML = "";
var tree = document.createTreeWalker(document.body);
while (tree.nextNode()) {
    text = tree.currentNode.nodeValue;
    if (tree.currentNode.nodeType == 3){
        text = text.replace(/(\W)(\w+)/g, '$1<element onmouseover="sendWord(\'$2\')">$2</element>');
        text = text.replace(/^(\w+)/, '<element onmouseover="sendWord(\'$1\')">$1</element>');
        }
    newHTML += text
}
document.body.innerHTML = newHTML;

Edit: I realize a better workaround would be to custom tag the text nodes ((Customtag_Start_Here) etc.), copy the whole DOM to a string, and use my customs tags to identify text nodes and modify them that way. But if I don't have to, I'd rather not.

like image 698
Norbles Avatar asked Jan 11 '23 02:01

Norbles


1 Answers

To 'change' a text node into an element, you must replace it with an element. For example:

var text = tree.currentNode;
var el = document.createElement('foo');
el.setAttribute('bar','yes');
text.parentNode.replaceChild( el, text );

If you want to retain part of the text node, and inject an element "in the middle", you need to create another text node and insert it and the element into the tree at the appropriate places in the tree.


Edit: Here's a function that might be super useful to you. :)

Given a text node, it runs a regex on the text values. For each hit that it finds it calls a custom function that you supply. If that function returns a string, then the match is replaced. However, if that function returns an object like:

{ name:"element", attrs{onmouseover:"sendWord('foo')"}, content:"foo" }

then it will split the text node around the match and inject an element in that location. You can also return an array of strings or those objects (and can recursively use arrays, strings, or objects as the content property).

Demo: http://jsfiddle.net/DpqGH/8/

function textNodeReplace(node,regex,handler) {
  var mom=node.parentNode, nxt=node.nextSibling,
      doc=node.ownerDocument, hits;
  if (regex.global) {
    while(node && (hits=regex.exec(node.nodeValue))){
      regex.lastIndex = 0;
      node=handleResult( node, hits, handler.apply(this,hits) );
    }
  } else if (hits=regex.exec(node.nodeValue))
    handleResult( node, hits, handler.apply(this,hits) );

  function handleResult(node,hits,results){
    var orig = node.nodeValue;
    node.nodeValue = orig.slice(0,hits.index);
    [].concat(create(mom,results)).forEach(function(n){
      mom.insertBefore(n,nxt);
    });
    var rest = orig.slice(hits.index+hits[0].length);
    return rest && mom.insertBefore(doc.createTextNode(rest),nxt);
  }

  function create(el,o){
    if (o.map) return o.map(function(v){ return create(el,v) });
    else if (typeof o==='object') {
      var e = doc.createElementNS(o.namespaceURI || el.namespaceURI,o.name);
      if (o.attrs) for (var a in o.attrs) e.setAttribute(a,o.attrs[a]);
      if (o.content) [].concat(create(e,o.content)).forEach(e.appendChild,e);
      return e;
    } else return doc.createTextNode(o+"");
  }
}

It's not quite perfectly generic, as it does not support namespaces on attributes. But hopefully it's enough to get you going. :)


You would use it like so:

findAllTextNodes(document.body).forEach(function(textNode){
  replaceTextNode( textNode, /\b\w+/g, function(match){
    return {
      name:'element',
      attrs:{onmouseover:"sendWord('"+match[0]+"')"},
      content:match[0]
    };
  });
});

function findAllTextNodes(node){
  var walker = node.ownerDocument.createTreeWalker(node,NodeFilter.SHOW_TEXT);
  var textNodes = [];
  while (walker.nextNode())
    if (walker.currentNode.parentNode.tagName!='SCRIPT')
      textNodes.push(walker.currentNode);
  return textNodes;
}

or if you want something closer to your original regex:

  replaceTextNode( textNode, /(^|\W)(\w+)/g, function(match){
    return [
      match[1], // might be an empty string
      {
        name:'element',
        attrs:{onmouseover:"sendWord('"+match[2]+"')"},
        content:match[2]
      }
    ];
  });
like image 98
Phrogz Avatar answered Jan 29 '23 02:01

Phrogz