CPANdeps + Greasemonkey

brian_d_foy on 2007-12-15T17:50:00

I've just written a quick and dirty Greasemonkey script that adds a link to David Cantrell's excellent CPAN dependencies to any CPAN distribution's page on search.cpan.org.

// ==UserScript==
// @name           CPAN Search
// @namespace      http://hexten.net/
// @description    Add links to CPAN
// @include        http://search.cpan.org/
// ==/UserScript==

var new_links = {
    'CPAN Dependencies': function(url, name) {
        return 'http://cpandeps.cantrell.org.uk/?module='
            + escape(make_module_name(name));
    }
};

function canonical_url() {
    var permalink = document.getElementById('permalink');
    if (permalink) {
        return permalink.firstChild.href;
    }
    return '';
}

function trim_url(url) {
    return url.replace(/^http:\/\/[^\/]+\/[^\/]+\//, '').replace(/\/$/, '');
}

function make_module_name(dist_name) {
    return dist_name.replace(/-/, '::');
}

function add_links(nd) {
    var end = nd.lastChild;
    nd.removeChild(end);
    var dist_url  = canonical_url();
    var dist_name = trim_url(dist_url);
    // console.log(dist_name + ' ' + dist_url);
    var keys = [];
    for (var k in new_links) {
        keys.push(k);
    }
    keys = keys.sort();
    for (var l = 0; l < keys.length; l++) {
        nd.appendChild(document.createTextNode(" ]\n[ "));
        var name = keys[l];
        var link = document.createElement('A');
        link.href = new_links[name](dist_url, dist_name);
        link.innerHTML = name;
        nd.appendChild(link);
        // console.log(name + " " + link);

    }
    nd.appendChild(end);
}

var rows = document.getElementsByTagName('tr');
if (rows) {
    for (var r = 0; r < rows.length; r++) {
        var cells = rows[r].getElementsByTagName('td');
        if (cells.length == 2 && cells[0].innerHTML == 'Links') {
            add_links(cells[1].firstChild);
        }
    }
}

I'm sure the DOM walking can be improved - but it works. To add other links add them to the new_links hash. Each entry is the anchor text for the link and a function that returns the URL to link to.


Ahem

AndyArmstrong on 2007-12-15T18:59:32

// @include http://search.cpan.org/*

The asterisk is important :)

Very cool!

Yanick on 2007-12-15T23:21:13

Me likes!

And don't forget to add it on Userscripts.org.

XPath insead of manual DOM walking

Ven'Tatsu on 2007-12-16T05:32:46

I prefer using XPath expressions when I can, for much the same reason that I prefer using regular expressions to building my own pastern matching code. To get the same results as your DOM code I would use the following.

var result = document.evaluate(
    '//tr[td[last()=2]][td[1]/text()="Links"]/td[2]',
    document,
    null,
    XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
    null
);
for (var i = 0; i < result.snapshotLength; i++) {
    add_links(result.snapshotItem(i).firstChild);
}
If I could assume that there is only one desired node and I that would not need the restriction that the number of cells in the row be exactly 2, I would use this.

var result = document.evaluate(
    '//tr[td[1]/text()="Links"]/td[2]',
    document,
    null,
    XPathResult.FIRST_ORDERED_NODE_TYPE,
    null
);
var cell = result.singleNodeValue;
if (cell) {
    add_links(cell.firstChild);
}
document.evaluate is not available in all browsers so I would not use it in general purpose code, I think that using it in a userscript is safe though.

Re:XPath insead of manual DOM walking

Aristotle on 2007-12-16T07:21:33

I think that using it in a userscript is safe though.

Yes, it’s Firefox (and maybe Opera) only, which supports XPath.

so I would not use it in general purpose code

Indeed; in general purpose code you would use jQuery (or some other DOM query library of your preference) to write the same thing with CSS3 selectors. This particular case is not as concise as the XPath version because it needs to check text content, which CSS largely has no means to do.

For comparison’s sake, if written in jQuery, the entire DOM manipulation required would look something like this (untested!):

var link_node = ( function() {
    var dist_url  = canonical_url();
    var dist_name = trim_url(dist_url);

    var link_label = [];
    for ( var k in new_links ) link_label.push( k );

    return jQuery.map( link_label.sort(), function() {
        $('<a></a>')
            .attr( 'href', new_links[ this ]( dist_url, dist_name ) )
            .text( label );
    } );
} )();

jQuery( 'tr > td:nth-child(1)' )
    .filter( function() { return $( this ).text() == 'Links' } )
    .parent()
    .find( 'td:nth-child(2)' )
    .each( function() {
        var cell = $( this );
        jQuery.map( link_node, function() {
            cell
                .append( "\n[ " )
                .append( this )
                .append( " ]" );
        } );
    } );

Re:XPath insead of manual DOM walking

Aristotle on 2007-12-17T01:26:38

Woops. Replace the remaining $() calls with jQuery(). I haven’t trained myself out of the habit completely yet.

Re:XPath insead of manual DOM walking

AndyArmstrong on 2007-12-16T12:01:59

Thanks for that - much tidier.

I'm playing with HTTP::Proxy - in fact working on HTTP::Proxy::GreaseMonkey at the moment so I think I'll leave my version the same until I find out whether Safari does XPath. I'm guessing it doesn't. The current script is now working in Safari for me :)

Re:XPath insead of manual DOM walking

AndyArmstrong on 2007-12-16T19:16:10

Here's the announcement of HTTP::Proxy::GreaseMonkey.

Mashup escalation war

Yanick on 2007-12-16T21:25:17

I've tacked the following at the end of your userscript:

var deps = document.evaluate(
    "//a[contains(text(),'CPAN Dependencies')]",
    document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null );

if ( deps.snapshotLength > 0 ) {
   GM_xmlhttpRequest({
        method: "GET",
        url: deps.snapshotItem(0).href,
        onload: function ( resp ) { mashupDeps( resp, deps ) }
    });
}

function mashupDeps ( resp, deps ) {
    var x = resp.responseText.replace( /\n/g, " " );
    var match = x.match( /Chance of success.*?(\d+%)/ );
    for ( var i = 0; i < deps.snapshotLength; i++ ) {
        deps.snapshotItem(i).innerHTML += " (" + match[1] + ")" ;
    }
}

Now, the CPAN Dependencies link also (eventually) shows the running chances percentage. If we can persuade DrHyde to add an xml interface to the results, we could probably do even higher magic with them. :-)

Re:Mashup escalation war

AndyArmstrong on 2007-12-16T21:38:55

Heh :)

Now I need to implement GM_xmlhttpRequest in HTTP::Proxy::GreaseMonkey so it works in Safari too...

Re:Mashup escalation war

Yanick on 2007-12-17T04:26:15

Oh my. On one hand, it should not be impossible, as it's all Javascript and one only has to dig in GreaseMonkey's code to find what's needed. On the other hand, I just peeked at the said GM code, and it's a pretty substancial amount of code. Something tells me it's going to be quiiiite an interesting feature to implement. Good thing that the Christmas vacations and insane quantities of eggnog are just around the corner... :-)

Re:Mashup escalation war

AndyArmstrong on 2007-12-17T18:09:27

I've just released 0.03 which supports GM_xmlhttpRequest and runs your modified version of the script on Safari and FireFox.

And to think Aristotle said it couldn't be done :)

Re:Mashup escalation war

Yanick on 2007-12-17T18:19:03

Doing the impossible. Within 12 hours of the challenge's inception.

Whoa. Your script-fu *is* strong. 8-o

Re:Mashup escalation war

drhyde on 2008-01-04T12:06:58

Your wish is my command. But if the XML interface starts getting hammered then I might either rate-limit it, or remove it, or change the interface, or paint it purple, or ...

Re:Mashup escalation war

Yanick on 2008-01-29T01:07:27

Your wish is my command.

Oooh... Very cool... Thanks!

*hack* *hack* *tinker* *hack*

Here! What do you think of this: CPAN_Dependencies?

Re:Mashup escalation war

AndyArmstrong on 2008-01-29T01:40:41

Very cool :)

Re:Mashup escalation war

Yanick on 2008-01-29T01:52:53

Thanks. I must say I'm having a lot of fun tinkering with what is, basically, the CPAN edition of "Pimp My Ride". :-)

Re:Mashup escalation war

drhyde on 2008-01-30T12:06:04

It doesn't want to install for me (or possibly I just don't know how to install it), so I have no idea :-) but if there's anything I can do to the XML to make it work better for you, please let me know. And I'll add a link to the script from my site shortly.

Re:Mashup escalation war

Yanick on 2008-01-30T13:41:33

It doesn't want to install for me (or possibly I just don't know how to install it)

Drat! Hmm... Silly question, but, you do have Greasemonkey installed, right?

if there's anything I can do to the XML to make it work better for you, please let me know.

Thanks! I'll remember that. :-)

I'll add a link to the script from my site shortly.

Excellent. Fame, here I come!

Re:Mashup escalation war

drhyde on 2008-01-30T15:09:57

you do have Greasemonkey installed, right?
Errm, no. I thought it was part of Firefox.

Re:Mashup escalation war

Yanick on 2008-01-30T17:32:24

Heh. Good thing I remembered the golden rule of debugging: "first, always check if it's plugged in". ;-)

But yes, if you want the script to work, you have to install GreaseMonkey, or use the cool proxy that Andy came up with (http://search.cpan.org/user/andya/HTTP-Proxy-GreaseMonkey-0.05)

Re:Mashup escalation war

drhyde on 2008-02-05T22:49:02

Looks like Graham's done the ultimate escalation and added a link to cpandeps to all the distribution pages. Unfortunately, the machine it's on at the moment can't handle the traffic. Newer, studlier, stronger, faster, better server coming soon.

Re:Mashup escalation war

AndyArmstrong on 2008-02-05T22:51:35

D'you need some server space for it Dave?

Re:Mashup escalation war

drhyde on 2008-02-05T23:05:26

Thanks, but I've already ordered a machine from these people.

Re:Mashup escalation war

drhyde on 2008-02-07T21:08:05

The studly new server has now come and it all works again (blahblah DNS caching). Rejoice!