Simon Willison’s Weblog

plinks—a purple numbers variant

Via Tim Bray, I came across the concept of Purple Numbers. In a nutshell these are permalinks attached to every paragraph on a page which, to paraphrase Tim, make every paragraph on a page a first-class Web citizen.

That’s a very worthy concept, but the implementations I’ve seen have so far failed to inspire me. First of all, while the ability to link to any paragraph on a page is useful, the links themselves are either ugly, distracting or both. While reading Tim’s entry I found myself mentally pausing after each paragraph: probably because I’m used to the purple # marks on Scripting News and other such sites designating the end of an entry. They’re also extra cruft in my markup.

So, my ideal purple numbers implementation would minimise markup pollution and visual clutter.

Another issue with purple numbers is permanency: they’re absolutely no good if they don’t stay as true permalinks. This rules out naively generating them on the fly when a page is outputted as future edits to an article could result in links targetting different paragraphs entirely. Instead, the links (in the form of id attributes on paragraph tags) need to be assigned when the content is created. If additional paragraphs are later added to the content they should be numbered in such a way as not to intefere with the original paragraph links, which I shall call plinks for the sake of brevity.

We’ll ignore the issue of visual clutter for the moment: let’s look instead at how plinks can be introduced without polluting the markup of my pages. While the IDs that form the target of the links are a critical part of the structure of the page, the actual links are something of a convenience for people who don’t want to dig through my source code looking for IDs and are unaware of the various bookmarklets that can reveal them (such as Jesse Ruderman’s named anchors). As such, I don’t see the links as a critical part of the page content, so I have no qualms whatsoever about appending them to the page using JavaScript after the page has loaded. Here’s the function I’m using:


function addpLinks() {
  var paras = document.getElementsByTagName('p');
  for (var i = 0; i < paras.length; i++) {
    var current = paras[i];
    if (/^p-/.test(current.id)) {
      // It's a purple link paragraph
      var plink = document.createElement('a');
      plink.href = document.location.href.split('#')[0] + 
        '#' + current.id;
      plink.className = 'plink';
      plink.appendChild(document.createTextNode(' #'));
      current.appendChild(plink);
    }
  }
}

The function iterates over every paragraph on the page looking for paragraphs with an id that starts with “p-”, my chosen format for plink IDs. When it finds one, it creates a new link using the DOM and assigns it an href attribute which is the base URL of the current page (not including any existing fragment identifier) with a # and the paragraph’s ID appended on the end.

My plinks all have a class of “plink”, which allows me to style them. This is where I can reduce the visual clutter on the page as much as possible. Consider the following:


p a.plink {
  text-decoration: none;
  color: #c8a8ff;
  display: none;
}
p:hover a.plink {
  display: inline;
}

In an ideal world this would make the links invisible until the mouse cursor was positioned over the containing paragraph. Unfortunately, IE for Windows only honors the :hover pseudo-selector when it is applied to links. I’d like IE users to have at least a chance of discovering my plinks, so I came up with this:


p a.plink {
  text-decoration: none;
  color: #fff; /* the page background colour */
}
p:hover a.plink, p a:hover.plink {
  color: #c8a8ff;
}

The plinks are initially invisible by virtue of having the same colour as the page background. In browsers that support :hover on paragraphs, they become visible (by changing colour) when the mouse hovers over the paragraph. In browsers that only support :hover on links, they become visible when the mouse hovers over the links. Sure, they’re a lot harder to find but I see it as an easter egg for IE users. Another example of MOSe in action.

There are a couple of more pieces to the puzzle. Firstly, adding all of those IDs to those paragraph tags is the kind of task that humans avoid and computers thrive on. Now I could automate this in my CMS, but I’m not in the mood for PHP at the moment so I’ve automated it in a bookmarklet instead: Add plink IDs (drag to your bookmarks). The bookmarklet will look inside any textareas on the current page and add an ID to every paragraph, provided it’s a simple <p>. It’s something of a quick hack but it does the job. Here’s the bookmarklet code expanded to show how it works:


javascript:(function() {
  var tas = document.getElementsByTagName('textarea');
  for (var i = 0; i < tas.length; i++) {
    var ta = tas[i];
    var text = ta.value.replace('<p>', function() {
      if (typeof arguments.callee.counter == 'undefined') {
        arguments.callee.counter = 0;
      }
      return '<p id="p-'+arguments.callee.counter++ +'">';
    });
    ta.value = text;
  }
})();

Incidentally, the above uses a technique I picked up today while flicking through David Flanagan’s eternally useful JavaScript: The Definitive Guide. Inside a JavaScript function a special object called arguments is available. The object has a property called callee which refers to the function itself, even if as above it’s an anonymous function. Since functions are objects they can have properties: in this case, I create a counter property and use it to keep track of the IDs as I assign them. The whole lot is contained within a function argument to a replace call, where the function is called every time a <p> is found to determine what to replace it with.

At this point I had everything I needed, but then inspiration struck: how about a method of highlighting a paragraph if a user should visit a page using a link that targetted it? Suporting this meant adding yet another function to be executed once the page had loaded:


function plinkHighlight() {
  if (/#p-/.test(document.location)) {
    // The user arrived via a plink
    var plink_id = document.location.split('#')[1];
    var para = document.getElementById(plink_id);
    para.className = para.className + ' plinkHighlight';
  }
}

A custom style for the highlighted paragraph can now be defined using the plinkHighlight class hook.

I’ve now implemented all of the above on this site (mostly in the file plinks.js) although currently this is the only entry that contains plink IDs. Best of all, I didn’t have to touch a single line of my CMS! This JavaScript thing could really catch on some day.

This is plinks—a purple numbers variant by Simon Willison, posted on 30th May 2004.

Next: A few more thoughts on plinks

Previous: Time to fix those broken pages

Previously hosted at http://simon.incutio.com/archive/2004/05/30/plinks