Paul Graham click-to-inline footnotes

By Johan Sundström Last update Mar 16, 2007 — Installed 358 times. Daily Installs: 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
// ==UserScript==
// @name          Paul Graham click-to-inline footnotes
// @version       1.3: Bugfix: never inject the same footnote twice.
// @version       1.2: More resilient; registers only one listener, to body.
// @version       1.1: Added linkage both ways, and paulgraham.com inclusion.
// @namespace     http://www.lysator.liu.se/~jhs/userscript
// @description   Allows you to click footnote references at paulgraham.com to bring them to your eyes, rather than bring your eyes to the footnotes. (And afterwards finding your way back.)
// @include       http://paulgraham.com/*
// @include       http://www.paulgraham.com/*
// ==/UserScript==

var footnote_links = '//a[starts-with(@href, "#f")][font]';
var r1 = 'preceding::text()[1]';
var r2 = '(following-sibling::a[@name][1] | following-sibling::b[1])/' +
          'preceding-sibling::br[2]';

EventMgr = // avoid leaking event handlers
{
  _registry:null,
  initialize:function() {
    if(this._registry == null) {
      this._registry = [];
      EventMgr.add(window, "_unload", this.cleanup);
    }
  },
  add:function(obj, type, fn, useCapture) {
    this.initialize();
    if(typeof obj == "string")
      obj = document.getElementById(obj);
    if(obj == null || fn == null)
      return false;
    if(type=="unload") {
      // call later when cleanup is called. don't hook up
      this._registry.push({obj:obj, type:type, fn:fn, useCapture:useCapture});
      return true;
    }
    var realType = type=="_unload"?"unload":type;
    obj.addEventListener(realType, fn, useCapture||false);
    this._registry.push({obj:obj, type:type, fn:fn, useCapture:useCapture});
    return true;
  },
  cleanup:function() {
    for(var i = 0; i < EventMgr._registry.length; i++) {
      with(EventMgr._registry[i]) {
        if(type=="unload")
	  fn();
        else {
	  if(type=="_unload") type = "unload";
	  obj.removeEventListener(type,fn,useCapture);
        }
      }
    }
    EventMgr._registry = null;
  }
};

foreach( footnote_links, arm_footnote_injector );
EventMgr.add( document.body, 'click', inline_linked_footnote, true );

function arm_footnote_injector( a ) {
  var note = get_footnote( a );
  a.name = note.back;
  note.target.href = '#' + note.back;
}

function get_footnote( a ) {
  var id = a.hash.substr( 1 );
  return { id:id, back:id+'-back', target:document.anchors.namedItem( id ) };
}

function inline_linked_footnote( e ) {
  var node = e.target;
  var a = node.parentNode; // as we got the font[parent::a] tag
  if( !node.nodeName.match( /font/i ) || !a.pathname == location.pathname )
    return; // not a footnote reference
  var note = get_footnote( a );
  if( a.nextSibling.data != ']' )
    return; // already lifted in the footnote

  e.preventDefault();
  e.stopPropagation();
  var footnote = copy_between( r1, r2, note.target );

  var down = footnote.firstChild; // former target anchor
  down.href = '#'+ note.id;
  down.name = note.back;

  a.parentNode.replaceChild( footnote, a );
}

function trace() {
  unsafeWindow.console && unsafeWindow.console.trace();
}

function log() {
  unsafeWindow.console && unsafeWindow.console.log.apply( this, arguments );
}

function copy_between( start, end, node ) {
  var range = document.createRange();
  range.setStartAfter( typeof start == 'string' ? $X( start, node ) : start );
  range.setEndBefore( typeof end == 'string' ? $X( end, node ) : end );
  return range.cloneContents();
}

function foreach( xpath, cb, root ) {
  var results = $x( xpath, root ), node, i;
  if( results )
    for( i = 0; node = results[i]; i++ )
      cb( node, i );
}

function $X( xpath, root ) {
  var got = $x( xpath, root );
  return got instanceof Array ? got[0] : got;
}

function $x( xpath, root ) {
  try {
  var doc = root ? root.evaluate ? root : root.ownerDocument : document;
  var got = doc.evaluate( xpath, root||doc, null, 0, null ), next, result = [];
  switch( got.resultType )
  {
    case got.STRING_TYPE:
      return got.stringValue;
    case got.NUMBER_TYPE:
      return got.numberValue;
    case got.BOOLEAN_TYPE:
      return got.booleanValue;
    default:
      while( next = got.iterateNext() )
	      result.push( next );
      return result;
  }
  } catch( e ) {
    trace();
    log( e );
  }
}