2005-08-29

Adopting and adapting AutoLink

It appears I've fallen back on reading Jesse Ruderman this summer, and on catching up again, I stumbled upon AutoLink, a userscript which linkifies things that looks like links, or that ought to become links (ISBN numbers, mail addresses, bugzilla ticket numbers et cetera). I haven't thought about that kind of script since my (web)server side programming days, some time on the other side of Y2k, but by now you can have as much, and indeed even more fun in client land as you only could server side back in those days. (Javascript was not by far as much fun then, either, but XMLHttpRequest, the DOM and emerging web standards have mended much of that since. :-)

Anyway, I was a bit surprised to find I'd be the first who would want to do contextual autolinking - linking bug id:s referred from web pages at work to the company Trac installation, bug id:s at Lysator pages to bugzilla.lysator.liu.se, and so on, but the adding that capability was a sheer joyful five minute task.

In short, AutoLink processes pages you visit for a list of regexp filters, trying each in turn for matches, and stylishly converts the matched text portion to a link. Each such filter is denoted by a name, a regexp, and a function returning a link, more or less creatively based on the matched text portion. I added an optional additional regexp scope parameter, limiting the match to only be considered for URLs matching the regexp, when given.

And that was it. I'll host my version here, but expect you might want to stick to Jesse's for future updates (there has got to be more people than me who feed things back up the pipe). Well, time I sent back my diff.

Something that has been puzzling me a bit since I started really getting to grips with GreaseMonkey, is that at least my 0.5.1 Final installation doesn't seem to want to play nice with the horde of user scripts out there who install themselves on the "load" event listener, with, typically, a window.addEventListener("load", init, false); call. Was there some unannounced API change to GreaseMonkey only trig scripts after the load event was already fired, rendering all such scripts ineffective? If so, I suppose it is a change for the better, script writing usability wise, but it might be a good idea firing off a faked load event to user scripts made for earler versions, if that is fasible.

My diff doesn't feature the change, but if my linked userscript won't work with your installation it just might be because I dropped the event listening portion from it.
Categories:

2005-08-28

Blogger: writing proper plurals with CSS 2

Looking for something very unrelated, I stumbled upon an article on how to tweak blogger pages to render proper plurals for comments (0 comments, 1 comment, 2 comments, ...). It was a particularly smelly javascript solution, in that it wouldn't write anything at all for javascript disabled browsers. Poor style.

And while doing anything like this client side is kind of sad, I got this sudden silly urge of doing it right, and came up with a CSS 2 selectors solution in the spirit of, well, I don't know what. Just mark any place with a plural in it with a <span class="plural" name="<$BlogItemCommentCount$>"><$BlogItemCommentCount$> comment</span> tag (<$BlogItemCommentCount$> being the server-side generated number you want to act on to find plurals) and tuck in this bit of CSS in your stylesheet somewhere:

.plural[name]:after    { content:"s"; }
.plural[name="1"]:after { content:""; }

and behold! :-) I use the same trick to tag my user scripts with a little GreaseMonkeyGreaseMonkey icon, by the way, so I just have to type <a class="userscript" href="....user.js"> whenever I tuck in a link to some new user script I've conjured up. Comfy.

But back to plurals. Downside? Internet Explorer (at least up to present day) doesn't grok CSS 2 selectors. So to get it working in at least as many (and probably more) browsers, we get to add a bit of javascript to post-process the page if needed:

<script type="text/javascript"><!--
// in case your browser is crummy and doesn't grok CSS 2 selectors:
function fixPlural()
{
var ua = navigator.userAgent, plurals, i, node;
// tweak <span class="plural" name="N">N Comment</span> blocks?
if( /MSIE [0-6]/.test( ua ) && !/compatible/i.test( ua ) )
{
plurals = document.getElementsByTagName( 'span' );
for( i=0; i<plurals.length; i++ )
if( /plural/.test( (node=plurals[i]).className ) && node.name != 1 )
{
node.innerHTML += 's';
// and just in case this browser does support CSS2, toss the class
node.className = node.className.replace( /plural/, '' );
}
}
}//--></script>

and then find your <body> tag and rewrite it to <body onload="fixPlural();">. Congratulations on your human readable blog! :-)

Blogger: edit template button user script

Just to play around a bit with Aardvark to do something creative, I thought I'd find a nice spot in my template to drop an Atom Feed icon. Having been rather frequent on the edit template page recently, I've also started to find it somewhat inaccessible, hidden deep in the depths of probably several page clicks away from where I am when I want to do something with it.

Common blogspot page header So there was a slight delay, and I made another user script, figuring, hey, if others share my concern, why not spend a quarter on doing it nicely once and for all? Since you already have your own blogger blog (at least if you are interested in this user
script), you're not particularly likely ever use the "Get your own blog" link in the common blogger header again. So let's replace it with another one, heading straight off to the template editor.

Upgraded blogspot page header The graphics dabbling was the big part of the job, but I like the feel of the finished script - the nice blend-in graphics makes for that finishing touch that does all the difference. Platypus, for all its supposed splendour (it does seem able to do many nifty things once you grok its user interface -- in fact, I tried using it to speed up the creation of this hack), ruins most of it early on with its hideous toolbar (in the default setup) -- which you fortunately can switch off to just go with a nice little icon somewhere in your own toolbars. But user impressions is a large part of an application. One of the reasons MacOS will most likely always outshine Windows.

But I digress. I actually had another thing on my mind, too -- before putting the final userscript together I drew up a quick scriptlet which addresses the same issue. In fact, it's perhaps even more useful to some of you, since it takes you to the template editor right away, from any blogspot entry, regardless of whether you kept the blogger page header or tossed it. Feel free to bookmark that instead, or too.

And should you want to contribute graphics for the edit button in any of the other blogger provided styles; tan, silver, black and what have you, I'll willingly update the script to pick the appropriate graphics and host the images for it, as well. My original graphics is available in PNG and Photoshop formats.

...I think I'll get back to adding that Atom Feed link now. :-)

Aardvark

This is just a beautiful tool. I instantly fell in love with it -- switch it on for a page you're visiting, and go about exploring the entire DOM just by hovering any portion to see the element below (and its class and id attributes), and a red border marking its boundaries. Tap W to widen the scope to the next surrounding element, again, again, ...or N to narrow back a step at a time. Tap V to view the source of the element in a nicely colored and indented popup window you can cut and paste relevant portions from. Highlighting follows focused element boundaries -- in short, it's all the DOM Inspector isn't, when it comes to instant and response and usability. Some other nifty features strewn about, for good measure too, but I'll leave some of the excitement for you to find it out, yourselves. This is the kind of company you'd want to work for if planning on going about a geek career.

Where's the Paypal Donate button when for once I really most definitely want to?

Pure developer love to you, Karamatics! And as for the rest of you, what are you waiting for? Go try it out live right now, and install it after realizing you love it too. There there, off you go!
Categories:

2005-08-27

Utter wastes of time

face Today I've been wasting time and good weather on useless GreaseMonkey facial tweaking, and quite literally too. It's a customization of Lunarstorm, a mostly teenish community I can't seem to drop out of, from having a bit too many friends there - and once in a while I come up with some Idea I just have to try, and then spend way too much time on doing it. Like today.

Some time ago, I made an earlier version of this script (left featured screenshot), which worked (it drops in a headshot of a user in their public guest book, in a space usually reserved for ads) -- but it wasn't very good-looking -- the bottom of the field had some ugly white slack, and the top some ugly gray. Besides, with the field a bit thinner than the images, it was a bit troublesome loading the image in a separate tab to see the whole of it - or indeed save it right away, since I just put the image as a cell background.

Today I rewrote it to address those problems. The new version does indeed fix them all, but it was a daunting job, mostly from my not knowing that the CSS clip property just wouldn't do anything unless the item you were trying to clip also had a position:absolute; declaration. Whyever is that?! Thanks for that page, TopXML, by the way; despite not saying a word about it, it was most helpful.

Anyway, by spooking around a lot with a relatively positioned inner div (got to shift it a bit to the left, if we are to chip off a chunk from the left side of the image and still have it in the spot where the ad cell was) containing the clipped img tag and adding a link around it, the image was easy to view/save. First and by far most troublesome part done away with.

Then there was the bit of addressing the ugly white and gray fields. The top gray came away, after a bit of DOM Inspector research by shrinking the height of the element responsible for it by two pixels. The bottom was made by another table, though, which had bottom-left and bottom-right corners and a bottom background set to small gifs, which of course had the page background as the transparent portion, rather than the white bit. Oh, well, a bit of image editing and I had shiny new parts with the contents transparent and the background Lunar blue. Yaay.

First, I laborated a bit by setting the parent <tr> background to the appropriate part of the facial image, but I didn't get that to work, so after a while I attached it to the <table> itself, and behold, it worked!

faceSee how much better it looks? Okay, I'll admit it -- it's probably mostly due to the crummy image of me being replaced with an image of some cute random girl, but what the heck. It's her birthday and she got a car from some benevolent person or persons unknown -- of course she's bound to outshine a random geek such as myself, eh?
Categories:

2005-08-22

More calendar widgetry

This is a follow-up article to my recent entry on setting up Blogger with calendar navigation. I've been polishing it some more to better fit my preferences.

Actually, I was just following my own suggested further improvement, in adding the popup variant, which has some really nifty keyboard navigation added in, right out of the box --

  • ←, →, ↑, ↓ -- select date
  • CTRL + ←,→ -- select month
  • CTRL + ↑,↓ -- select year
  • SPACE -- go to today's date
  • ENTER -- go to the selected date
  • ESC -- cancel selection
-- but come that far, I noticed that all the disabled dates in view could still be focused, only just not visibly so. So it was really a bit like navigating with a paper with holes punched for the legal choices stuck in front of the screen. You'd notice when you hit an available date, but otherwise be generally lost. Not pretty.

So I set about tweaking the code to allow my preferred mode of browsing -- focus snapping only to dates that can actually be focused and selected. As it turns out, jscalendar is really a SourceForge project too, besides being LGPL, so I took the liberty of submitting a patch with my changes when I was happy with them.

An example setup is running on my real life blog; click the calendar icon in the sidebar to take it out for a spin, if I still haven't already updated the template in this blog too, by the time you read this.

So, what changes do you need to do to get this baby running?

Not much, really. It's mostly a matter of adding a method that will seek out a date to snap to, and provide it during Calendar setup. I did it like this:

  Calendar.setup(
{
step : 1, // show *every* year in the year menus
date : thisDate, // the date selected by default
align : 'br', // below/right from top/left corner
button : 'navigate', // click this element to open
onUpdate : dateChanged, // navigates to chosen entry
pickPrevNext : pickPrevNext, // can only focus our dates
dateStatusFunc: disableDateP // which dates to show/hide how
});

where most is kept intact from before (I've added an <img id="navigation" style="cursor:hand;cursor:pointer;" .../> to the mix, to trigger the popup, too -- the ugly style rule is to get a mouseover hint that works in most browsers too while hovering the icon).

You also have to provide the pickPrevNext method, though, and in my case it looks like this (together with the slightly changed disableDateP and init code):

// prevP == true: find the last value in ok to satisfy ok[n] <= pick,
// prevP != true: find the first value in ok to satisfy ok[n] >= pick
function pickPrevNext( pick, prevP )
{
var ok = top.notelist;
pick = pick.getTime();
if( prevP ) // find last x <= pick
{
for( var i=ok.length-1; i>=0; i-- )
if( ok[i] <= pick )
return new Date( ok[i] );
}
else // find first x >= pick
for( var i=0; i<ok.length; i++ )
if( ok[i] >= pick )
return new Date( ok[i] );
return null;
}

function disableDateP( date, y, m, d )
{
var now = new Date;
if( (y == now.getFullYear()) &&
(m == now.getMonth()) &&
(d == now.getDate()) )
return false;
var t = new Date( y, m, d );
return noteFromDate( t ) ? false : true;
}

function noteFromDate( date )
{
return top.notes[date.getTime()];
}

function renderCal()
{
var archive = document.getElementById( 'archive' );
if( archive )
{
archive.style.display = 'none';
var notes = {};
top.notelist = [];
var links = archive.getElementsByTagName( 'a' );
var i, j, node, date, y, m, d;
for( i=0; i<links.length; i++ )
{
node = links[i];
date = node.innerHTML.split('-'); // YYYY-MM-DD
y = parseInt( date[0], 10 );
m = parseInt( date[1], 10 );
d = parseInt( date[2], 10 );
t = (new Date( y, m-1, d )).getTime();
notes[t] = node.href;
top.notelist.push(t);
}
var dates = document.getElementsByTagName( 'h2' ), thisDate;
for( i=0; i<dates.length; i++ )
if( dates[i].className == 'date-header' )
{
var ymd = dates[i].innerHTML.split('-'); // YYYY-MM-DD
thisDate = new Date( parseInt( ymd[0], 10 ),
parseInt( ymd[1], 10 )-1,
parseInt( ymd[2], 10 ) );
break;
}
top.notes = notes;
// the above Calendar.setup() call goes here.
}
}

That wasn't very hard either, was it? Yes, one more thing -- I changed the init method to read renderCal, after having read in the docs that the popup code might want to zap window.calendar, hence not making that a very good name to use for our init method. So you'll want to update your body tag to read <body onload="renderCal()"> as well.

It's still not quite all I expect of it -- for instance, the dropdown menus don't adhere to the same navigation principle -- you can still pick months there which have no legal dates, or even if you pick one that does, you won't arrive at the note closest to the note you came from. I'll just have to leave that for another day, or another hacker.

2005-08-21

The Advogato XMLRPC API

Since I still had a bunch of old Advogato entries I'd like to archive, post-process (to get rid of their weird HTML injection mishaps) and upload to my blogger account I drew up a quick pike script to mirror mine, or for that matter, any other Advogato blog. It was easy enough - good work on the API, guys! A kindly meant suggestion to you, by the way -- you may find that introducing a commenting system would lose you fewer writers to competing sites. In my case, that was what finally got me to switch over to Blogger. It's just not worth posting quality tech stuff, unless the reader base can easily talk back and thus further elevate the quality of a post.

Anyway, in case anyone else would want to mirror a log or a few as well, here it is:

#! /usr/bin/env pike

constant address = "http://www.advogato.org/XMLRPC";

int main( int argc, array(string) argv )
{
Protocols.XMLRPC.Client client = Protocols.XMLRPC.Client( address );
for( int i=1; i<argc; i++ )
mirror( argv[i], client );
}

int mirror( string user, Protocols.XMLRPC.Client client )
{
Calendar.TimeRange ctime, mtime;
int entries, i;
string html, filename;
[entries] = client["diary.len"]( user );
for( i=0; i<entries; i++ )
{
[[ctime, mtime]] = client["diary.getDates"]( user, i );
[html] = client["diary.get"]( user, i);
string filename = user + "." + i + ".txt";
Stdio.write_file( filename, html + "\n" + ctime->format_time() );
System.utime( filename, ctime->unix_time(), mtime->unix_time() );
}
return entries;
}
Categories:

2005-08-20

Adding calendar navigation to your Blogger template

A later follow-up article is available on this topic, where I dig into making a pop-open calendar with keyboard bindings for navigating back and forth through your post archives, among other things.

Not caring much for Blogger's idea of navigation for my personal diary, today I set about adding a calendar widget similar to one I have been accustomed to having last time I was blogging, in a self-hosted do-it-all-yourself environment. Lazy-bastardness has changed that situation since, but I digress -- let's get down to business, shall we?

Someone probably made a decent calendar widget in the past few years, right? Right. Good; then I won't waste a lot of time boring my head off tweaking HTML+CSS, which I, quite honestly, detest.

Okay, download and unzip the 1.0 release, and dig in. First things first, we put it in a suitable location on some hosting of our own, something like http://www.example.com/jscalendar-1.0/ (you probably have it on some different location, yadda yadda) and toss in the necessary bits to add it to your template's <head> portion. I tucked mine in just before the end of the head section, since we will be tweaking other things in the vicinity very soon anyway. So here we go; add these lines:

<style type="text/css">@import url("/skins/aqua/theme.css");</style>
<script type="text/javascript" src="/calendar.js"></script>
<script type="text/javascript" src="/lang/calendar-en.js"></script>
<script type="text/javascript" src="/calendar-setup.js"></script>

(don't forget to prepend your URL prefix to the src/url arguments!) and we now have a beautiful bit of GNU LGPL calendar code loaded and ready, just aching to get to do something for us. So, pick a spot in your template where you want the finished calendar widget to go, and add an empty <div id="calendar-container"></div> tag there. If you're eager to try out a dummy right away, by all means go ahead: temporarily add this right after the div:

<script type="text/javascript"><!--
Calendar.setup( {flat:'calendar-container'} );
//--></script>

(Tada!)

Okay, not very interesting before it does anything; I agree. Before we continue, though, we need to tweak some settings (or you need to rewrite some of my code to fit your own preferences). First, track down Settings -> Archiving -> Archive Frequency and set it to "Daily". (You need this to get each entry in your blog listed in the page - and hence to show up in the calendar.) Then (and the order you perform these changes is important) set Settings -> Formatting -> Archive Index Date Format to 2005-08-20 (ISO YYYY-MM-DD format; very nice to parse). In case you're alienated by practical date formats, don't panic; the visitor will not see these dates anyway. Setting Date Header Format will however be visible, but my example code further down will assume you did, so if you are lazy or share my preferences you'll want to have ISO dates here, too. These are the dates shown near the head of a post in your average Blogger template, and they are used by the script to detect which date should be highlighted as "this post". But now I'm getting ahead of myself.

Now dive into your template and look up the archive portion - it's probably surrounded by a pair of <MainOrArchivePage></MainOrArchivePage> tags. Replace those with a <div id="archive"></div> tag (since you want your navigation widget on all pages, including the single-day's-posts page), and since we want the archive portion easily targetable for hiding.

That could be done with a CSS style rule #archive { display:none; }, but I opted to perform the hiding from javascript instead, knowing that there still are a few who navigate the web with javascript turned off. This way, you won't condemn them to live without navigation, even though they don't get the benefit of your spiffy DHTML generated-live calendar widget. A shame, but that's their choice, and it's very decent of you to degrade gracefully.

Make sure the contents of that div contains something like

<BloggerArchives>
<li><a href="<$BlogArchiveURL$>"><$BlogArchiveName$></a></li>
</BloggerArchives>

(the <li> and any other additional tags besides the <a> tag aren't needed, but don't hurt either). Just make sure the div swallows up every bit you want to hide for the crows who can see your nifty calendar. Now drop the proof-of-concept <script> tag you might have tossed in below the other div, and finally add this chunk at the end of your <head> tag:

<script type="text/javascript"><!--
function calendar()
{
var archive = document.getElementById( 'archive' );
if( archive )
{
archive.style.display = 'none';
var notes = {};
var links = archive.getElementsByTagName( 'a' );
if( !links.length ) return;
var i, j, node, date, y, m, d;
for( i=0; i<links.length; i++ )
{
node = links[i];
date = node.innerHTML.split('-'); // YYYY-MM-DD
y = parseInt( date[0], 10 ); if(!notes[y]) notes[y] = {};
m = parseInt( date[1], 10 ); if(!notes[y][m]) notes[y][m] = {};
d = parseInt( date[2], 10 ); notes[y][m][d] = node.href;
}
var dates = document.getElementsByTagName( 'h2' ), thisDate;
for( i=0; i<dates.length; i++ )
if( dates[i].className == 'date-header' )
{
var ymd = dates[i].innerHTML.split('-'); // YYYY-MM-DD
thisDate = new Date( parseInt( ymd[0], 10 ),
parseInt( ymd[1], 10 )-1,
parseInt( ymd[2], 10 ) );
break;
}
top.notes = notes;
Calendar.setup(
{
step : 1, // show every year in the year menus
date : thisDate, // selected by default
flat : 'calendar-container', // div element
range : [ parseInt(links[0].innerHTML), y ],
showOthers : true, // show whole first/last week of month
flatCallback : dateChanged, // what to do on date selection
dateStatusFunc: disableDateP // which dates to show/hide how
});
}
}

// Returns true for all dates lacking a note, false or a css style for those having one.
// Exception: today does not return true, even if it lacks a note. (improves navigation)
function disableDateP( date, y, m, d )
{
var now = new Date;
if( (y == now.getFullYear()) &&
(m == now.getMonth()) &&
(d == now.getDate()) )
return false;
return noteFromDate( date ) ? false : true;
}

function noteFromDate( date )
{
var note = top.notes[date.getFullYear()] || {};
note = note[date.getMonth()+1] || {};
return note[date.getDate()];
}

function dateChanged( calendar )
{
if( calendar.dateClicked )
{
var note = noteFromDate( calendar.date );
if( note )
window.location = note;
}
}//--></script>

and edit the <body> tag following it to read <body onload="calendar()"> -- and you're all set to go! Dates with entries on them are clickable, and you can navigate around among the years and months as you well please, without any time-consuming web server roundtrips too.

Of course, feel free to experiment, perhaps most easily with the parameters in the Calendar.setup() call, or indeed with any other aspect; having the date-header parsing code match your date listing preferences or whatnot. Another good exercise is probably trying out a layout where you add the widget as a popup rather than the flat embedded one - that way you'll get extremely nifty keyboard navigation via the arrow (and control) keys too in the widget.

Share and enjoy! Spread the love! :-) And feel free to link your creative results.

Note: you always have to click "Republish Entire Blog" when adding a new date that previously had no entry, if you want your visitors to be able to click back in time and then find their way back to the future again using the calendar. Remember, already published pages won't know of your latest entry otherwise, and will thus not make it clickable.

2005-08-19

Kingdom of Loathing cooking

This is vapour ware, in that there is of course no guarantee anything remotely like it will ever show up in the Kingdom itself (I'm only saying this because some might assume I've got some kind of Actual Influence, on what happens in the KoL development team -- which I don't; I hang around, for these occasional "Shiny! -- but wouldn't this be more neat still?" moments. End of disclaimer! :-).

That said, there were a few other "Oo, shiny!"ies in the thread, so I wouldn't be very surprised to see something like this some time. I personally wonder a bit if it's not a bit too client-side for Jick's tastes. Somehow I've got the feeling he is very reluctant to put any code that could be solved server-side on the browser's side. Good thing these things can't. :)
Categories:

2005-08-17

Generating an M3U playlist live from a scriptlet

While browsing around remix.kwed.org for something nice to play while working today, I realized I was missing a scriptlet to make an M3U play list of all mp3 files linked from any web page (and web plain web server directory listings in particular).

So I wrote one. I had estimated five minutes, but it probably took closer to ten, my usual with(document){open('content-type'); write(page); close()} proving less than useful in a scriptlet context, since the document replacing also tosses away all javascript state, including the generated playlist page to be written.

Enter the very convenient RFC 2397 data: URL protocol -- replacing that bit with a location='content-type,'+escape(page), and we're set. (The Mozilla data: testsuite is a handy resource to get a quick idea of what you can do with this toy and how, if you are new to the concept. It's still not widely supported in browsers, but good browsers do.)

Enjoy! - the Generate M3U playlist scriptlet, in all its glory.