开发者

What's a good alternative to HTML rewriting?

Consider this document fragment:

<div id="test">
    <h1>An article about John</h1>
    <p>The frist paragraph is about John.</p>
    <p>The second paragraph contains a <a href="#">link to John's CV</a&g开发者_运维问答t;.</p>
    <div class="comments">
        <h2>Comments to John's article</h2>
        <ul>
            <li>Some user asks John a question.</li>
            <li>John responds.</li>
        </ul>
    </div>
</div>

I would like to replace every occurrence of the string "John" with the string "Peter". This could be done via HTML rewriting:

$('#test').html(function(i, v) {
    return v.replace(/John/g, 'Peter');    
});

Working demo: http://jsfiddle.net/v2yp5/

The above jQuery code looks simple and straight-forward, but this is deceiving because it is a lousy solution. HTML rewriting recreates all the DOM nodes inside the #test DIV. Subsequently, changes made on that DOM subtree programmatically (for instance "onevent" handlers), or by the user (entered form fields) are not preserved.

So what would be an appropriate way to perform this task?


How about a jQuery plugin version for a little code reduction?

http://jsfiddle.net/v2yp5/4/

jQuery.fn.textWalk = function( fn ) {
    this.contents().each( jwalk );
    function jwalk() {
        var nn = this.nodeName.toLowerCase();
        if( nn === '#text' ) {
            fn.call( this );
        } else if( this.nodeType === 1 && this.childNodes && this.childNodes[0] && nn !== 'script' && nn !== 'textarea' ) {
            $(this).contents().each( jwalk );
        }
    }
    return this;
};

$('#test').textWalk(function() {
    this.data = this.data.replace('John','Peter');
});

Or do a little duck typing, and have an option to pass a couple strings for the replace:

http://jsfiddle.net/v2yp5/5/

jQuery.fn.textWalk = function( fn, str ) {
    var func = jQuery.isFunction( fn );
    this.contents().each( jwalk );

    function jwalk() {
        var nn = this.nodeName.toLowerCase();
        if( nn === '#text' ) {
            if( func ) {
                fn.call( this );
            } else {
                this.data = this.data.replace( fn, str );
            }
        } else if( this.nodeType === 1 && this.childNodes && this.childNodes[0] && nn !== 'script' && nn !== 'textarea' ) {
            $(this).contents().each( jwalk );
        }
    }
    return this;
};

$('#test').textWalk(function() {
    this.data = this.data.replace('John','Peter');
});

$('#test').textWalk( 'Peter', 'Bob' );


You want to loop through all child nodes and only replace the text nodes. Otherwise, you may match HTML, attributes or anything else that is serialised. When replacing text, you want to work with the text nodes only, not the entire HTML serialised.

I think you already know that though :)

Bobince has a great piece of JavaScript for doing that.


I needed to do something similar, but I needed to insert HTML markup. I started from the answer by @user113716 and made a couple modifications:

$.fn.textWalk = function (fn, str) {
    var func = jQuery.isFunction(fn);
    var remove = [];

    this.contents().each(jwalk);

    // remove the replaced elements
    remove.length && $(remove).remove();

    function jwalk() {
        var nn = this.nodeName.toLowerCase();
        if (nn === '#text') {
            var newValue;

            if (func) {
                newValue = fn.call(this);
            } else {
                newValue = this.data.replace(fn, str);
            }

            $(this).before(newValue);
            remove.push(this)
        } else if (this.nodeType === 1 && this.childNodes && this.childNodes[0] && nn !== 'script' && nn !== 'textarea') {
            $(this).contents().each(jwalk);
        }
    }
    return this;
};

There are a few implicit assumptions:

  • you are always inserting HTML. If not, you'd want to add a check to avoid manipulating the DOM when not necessary.
  • removing the original text elements isn't going to cause any side effects.


Slightly less intrusive, but not necessarily any more performant, is to select elements which you know only contain text nodes, and use .text(). In this case (not a general-purpose solution, obviously):

$('#test').find('h1, p, li').text(function(i, v) {
    return v.replace(/John/g, 'Peter');
});

Demo: http://jsfiddle.net/mattball/jdc87/ (type something in the <input> before clicking the button)


This is how I would do it:

var textNodes = [], stack = [elementWhoseNodesToReplace], c;
while(c = stack.pop()) {
    for(var i = 0; i < c.childNodes.length; i++) {
        var n = c.childNodes[i];
        if(n.nodeType === 1) {
            stack.push(n);
        } else if(n.nodeType === 3) {
            textNodes.push(n);
        }
    }
}

for(var i = 0; i < textNodes.length; i++) textNodes[i].parentNode.replaceChild(document.createTextNode(textNodes[i].nodeValue.replace(/John/g, 'Peter')), textNodes[i]);

Pure JavaScript and no recursion.


You could wrap every textual instance that is variable (e.g. "John") in a span with a certain CSS class, and then do a .text('..') update on all those spans. Seems less intrusive to me, as the DOM isn't really manipulated.

<div id="test">
    <h1>An article about <span class="name">John</span></h1>
    <p>The frist paragraph is about <span class="name">John</span>.</p>
    <p>The second paragraph contains a <a href="#">link to <span class="name">John</span>'s CV</a>.</p>
    <div class="comments">
        <h2>Comments to <span class="name">John</span>'s article</h2>
        <ul>
            <li>Some user asks <span class="name">John</span> a question.</li>
            <li><span class="name">John</span> responds.</li>
        </ul>
    </div>
</div>


$('#test .name').text(function(i, v) {
    return v.replace(/John/g, 'Peter');    
});

Another idea is to use jQuery Templates. It's definitely intrusive, as it has its way with the DOM and makes no apologies for it. But I see nothing wrong with that... I mean you're basically doing client-side data binding. So that's what the templates plugin is for.


This seems to work (demo):

$('#test :not(:has(*))').text(function(i, v) {
  return v.replace(/John/g, 'Peter');    
});


The POJS solution offered is ok, but I can't see why recursion is avoided. DOM nodes are usually not nested too deeply so it's fine I think. I also think it's much better to build a single regular expression than use a literal and build the expression on every call to replace.

// Repalce all instances of t0 in text descendents of
// root with t1
// 
function replaceText(t0, t1, root) {

  root = root || document;
  var node, nodes = root.childNodes;

  if (typeof t0 == 'string') {
    t0 = new RegExp(t0, 'g');
  }

  for (var i=0, iLen=nodes.length; i<iLen; i++) {
    node = nodes[i];

    if (node.nodeType == 1) {
      arguments.callee(t0, t1, node);

    } else if (node.nodeType == 3) {
      node.data = node.data.replace(t0, t1);
    }
  }
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜