Wednesday, May 7, 2008

Textnode translator for Javascript

Introduction

This is a generic JS Class, that allows you to translate(parse) the content of text nodes, and replace them for the new content.
You only need to specify the parsing function, and the starting (root) node.
All the text nodes inside it, will get parsed.

Modes

The class supports synchronous and asynchronous parsing.
The parsing function will receive the original text as first argument.
If you can parse the text right away, then just return it from the parsing function.
If it needs to be delayed (f.e: AJAX request), then the set the attribute 'sync' of the instance, to false. By doing this, you will get a second argument in the parsing function, which will be a function that you need to call, passing it the parsed text.

Returned data

The parsed data you return can be:
  • a string
    This is the standard, a string replacing the old one.
  • nothing
    new If you don't return data, or the same string, it will just get skipped.
  • a node
    new You can return an html node.
  • an array
    new You can return an array with nodes and/or strings.

Filteringnew

Optionally, you can pass the translator, a filtering function. This way you can exclude elements (and their descendants) from the parsing.
To use it, pass the function as second argument to the constructor.
It will receive the (element) node, and it must return true or false.

How to use

Check the demo to see both modes and filtering in action.
You need to call the method 'traverse' of the instance, passing it the root node.
Remember to call the method after the html document is parsed, so you can access all the nodes in it.

Links

Downloads

Update 5/26/08
Added 1.0.1, updated the docs and demo.

14 comments:

Joe said...

Nice Ariel! Will test against my own script I developed for this. Well done!

Cheers.

Joe

www.subprint.com

Godmar said...

Very nice. Three ideas for improvements:

- instead of having take Translator take a function, have it take an object and use a property of the object as callback function. This way, callers can add custom data to receive in the callback. (Use the same technique as in jQuery.ajax).

- for large pages, when this script is applied to the root node, it will freeze the page. To avoid this, split the processing into a configurable chunks that are processing in each configurable interval.

- finally, a feature that would exclude subtrees that are root in certain elements (such as <a>) would be nice.

These three features would significantly simplify the implementation of such functionality as autolinking (for a Firefox script that implements items #2 and #3, see Ruderman's autolinking script )

Ariel Flesler said...

Hi Godmar

Thanks for the suggestions:

@1: You can add the custom data to each translator instance. They will be the scope (this) of the parsing function.

@2: I need to think about it a bit. As it's recursive, it's not a straight forward change.
Note that you can call the translator many times, on different roots, using a delay between each call.

@3: I like it, I just saw the place to inject it with just a few more bytes.
Will add it asap.

Thanks

Godmar said...

ad 1) an object-based design seems more forward looking as it would keep the signature of the constructor identical as you add more functionality to your translator. For instance, your current code assumes that "parse" returns text that's then transformed into a text node.... but what if I'd like to, say, highlight a word? That would split the text node into multiple text nodes and elements.

ad 2) Instead of using recursion, you could use an array as a stack. Then pass the array to the continuation function. In each call, process n array elements.

Ariel Flesler said...

Added a new release!
Check the docs, changelog and demo to learn more about the update.

Added the filtering feature.

@1: You can now return an array, it'll solve that problem.

@2: Will consider it for 1.0.2.

Thanks!

amir said...

this was quite useful, thank you. i needed to write an incrementor plugin to increment all numbers within a set, but getting at the text nodes was challenging which your routine solved for me.

jQuery.fn.incr = function(amount, fn) {
amount = typeof amount == 'undefined' ? 1 : Number(amount);
var fn = fn || function(a){return amount + Number(a);}
var tr = new Translator(function( text ){
return text.replace(/(-?\d+)/g, fn );
});
this.each(function() {
tr.traverse(this);
})
return this;
}

Also added:

jQuery.fn.incrTo = function(amount, fn) {
return $(this).incr(amount, fn || amount);
}


Now I can just say $('.votes').incr(1);
$('.votes').incr(-2);
$('.votes').incrTo(0);

Thanks!

(Would be nice if .traverse could be passed a 'deep' argument!)

Ariel Flesler said...

Hi Amir, thanks for letting me know about the implementation.

The 'deep' argument you mention is to avoid making the traversing recursive ?

amir said...

yes. not terribly important, but would allow manipulating content from a more generic parent selector that may have other children we don't want to touch.

Ariel Flesler said...

It's doable... but note that the Translator instance accepts a 2nd argument that is a filter function.

Using that function, you can simply achieve what you mean, you return:

node.parentNode == your_root_node

X-MG said...

I have a problem, and I really need some tips to get it done.

My serialScroll works well, but my slides are html blocks with clickable links to go to specific pages, THE PROBLEM IS IF A CLICK ON THOSE LINKS THE ONLY THINK THAT HAPPENS IS THAT THE CONTENT SCROLLS BUT THE LINK DOES NOT SEND THE USER TO THAT PAGE.

Any ideas?

Thanks in advance!

Ariel Flesler said...

Hi

Set the option 'jump' to false. I think that should do.

Armand said...

Hi
how would i go about this I have a block of text and some parts of the text is in [p] [/p]

now i want to injectthe content in here back in the page in real paragraps.

Search and replace just shows the html as text and I would like it as html so that the page looks nice with paragraphs instead of one bulk of text

Ariel Flesler said...

Hi Armand

I'm not sure I follow, can you make some kind of demo of what you need ?
Sorry for the delay :(

Armand said...

Hi

that can happen but i worked it out myself here is the solution
var BlockP = new Translator(function( text ){
var data = text.replace( this.block, '*$3*' ).split('*');
if( data.length == 1 ) //
return data[0];
for( var i = 1; i < data.length; i += 2 ){
var block = data[i];
data[i] = document.createElement('p');
data[i].className='padded';
data[i].appendChild( document.createTextNode(block) );
}
return data;
}, function( node ){ // filter out links
return node.nodeName != 'A';
});
BlockP.block = /(\[(p)\])((?:.+))(\[\/(p)\])/g; // match