views:

625

answers:

3

I have the following code which works in FF / Chrome

var stack = [Array.prototype.slice.call(document.getElementsByTagName("body")[0].childNodes)], nodes, node, parent, text, offset;
while (stack.length) {
    nodes = stack.pop();
    for (var i=0, n=nodes.length; i<n; ++i) {
        node = nodes[i];
        switch (node.nodeType) {
            case Node.ELEMENT_NODE:
                if (node.nodeName.toUpperCase() !== "SCRIPT") {
                    stack.push(Array.prototype.slice.call(node.childNodes));
                }
                break;
            case Node.TEXT_NODE:
                text = node.nodeValue;
                offset = text.indexOf("[test=");
                if (offset >= 0 && text.substr(offset).match(/^(\[test=(\d+)\])/)) {
                    parent = node.parentNode;
                    var before = document.createTextNode(text.substr(0, offset));
                        link = document.createElement("a"),
                        after = document.createTextNode(text.substr(offset + RegExp.$1.length));
                    link.appendChild(document.createTextNode(text.substr(offset, RegExp.$1.length)));
                    link.setAttribute("href", "http://example.com/" + RegExp.$2);
                    parent.insertBefore(after, node);
                    parent.insertBefore(link, after);
                    parent.insertBefore(before, link);
                    parent.removeChild(node);
                    stack.push([after]);
                }
        }
    }
}

Basically what it does is if it finds [test=25] in the page it converts it to a link which points to example.com/25

In IE I get the following error: JScript Object Expected on first line:

var stack = [Array.prototype.slice.call(document.getElementsByTagName("body")[0].childNodes)], nodes, node, parent, text, offset;

This error occurs in both IE7 and IE8.

Any help would be appreciated.

Thanks.

+1  A: 

Try using this instead:

var stack = [Array().slice.call(document.getElementsByTagName("body")[0].childNodes)]

There's some funkyness with IE and prototype/constructors. I can't test right now, on a mac.

More info here: http://stackoverflow.com/questions/120804/difference-between-array-slice-and-array-slice/121302#121302

Infinity
+5  A: 

It's not legal to call Array.prototype.slice on a NodeList object as returned by the childNodes property (or various other DOM methods).

Normally it wouldn't be legal to call Thing.prototype.method on anything but an instance of Thing, however browsers have traditionally allowed — and the ECMAScript Third Edition standard requires — a special case for many Array.prototype methods so that they can be called on any native-JavaScript object which is sufficiently like an Array. This means, notably, that they can be used on the arguments object, which looks like an Array but actually isn't.

However, NodeList and the other collection objects in the DOM are not defined to be native JavaScript objects; they are allowed to be ‘host objects’, which are implemented completely by the browser and not the language. All bets are off for host objects...

Whether the slice function can be applied successfully to a host object is implementation-dependent.

So Array.prototype.slice may not work for NodeList, and in IE before version 8, indeed, it won't.

If you want to make a plain-Array copy of a NodeList, you'll have to do it the long but safe way:

Array.fromSequence= function(seq) {
    var arr= new Array(seq.length);
    for (var i= seq.length; i-->0;)
        if (i in seq)
            arr[i]= seq[i];
    return arr;
};

var stack = [Array.fromSequence(document.body.childNodes)];

Incidentally, you can make that linkifier a bit simpler by using textnode.splitText, and I'd be very wary about using the global RegExp properties, as if any unexpected regex work occurs in one of the intervening calls they'll be lost. Looking at the match object is usually better. See this question for another attack at basically the same problem.

bobince
+1. And purely as a coincidental side note: In some Firefox JavaScript benchmarks that I did recently, I found that constructing an array using an empty literal `[]` and `push` is, ironically enough, faster than the method shown here.
Justin Johnson
Interesting! `push` can't recreate a sparse-list (with missing items) as with the above, but for a NodeList you ain't gonna need it.
bobince
Thanks for the information and especially that link to that other question. It was exactly what I was looking for.
Rob
+2  A: 

I think this is because getElementsByTagname returns a nodelist - not an array (although some things like the [] operator work in that like they work on arrays, they are not the same)

Perhaps it can be solved in a less complicated way:

var els = document.body.getElementsByTagName("*");
for (var i=0, nulEls=els.length, el; i<numEls; i++){
    el = els.item(i);
    el.normalize();        
    for (var j=0, chs = el.childNodes, numChs=chs.length, ch; j++){
        ch = chs.item(j);
        if (ch.nodeType==Node.TEXT_NODE){
          //you code for replacing text with link goes here
          //ps i suggest using  ch.data instead of ch.nodeValue
        }
    }
}
Roland Bouman
Using `getElementsByTagName("*")` is a good idea, but remember this is a live NodeList: as you add links to the page, it'll receive extra items. This will mean the last number-of-added-elements items won't be checked (due to the `numEls`-remembering optimisation), and if the replacement text can contain the searched text it'll do a crazy recursive replacement. Better to take a copy of the nodelist as was original attempted, or simply to iterate the NodeList in reverse.
bobince
@bobince: indeed, you're right :) thanks for pointing that out.But in that case, I think I would try to store all textnodes that match in an array and then outside the loop, loop over the array again to do the replacement - I think that should be possible without needing any explicit node positions. I'll have to think about your suggestion to walk the list in reverse...I've been up too long and need sleep. But I will think about that - thanks!
Roland Bouman