Paul Graham click-to-inline footnotes

By Johan Sundström Last update Jul 28, 2010 — Installed 499 times.

There are 1 previous version of this script.

// ==UserScript==
// @name          Paul Graham click-to-inline footnotes
// @version       1.4: Also show HTML comments.
// @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.) Now with corresponding feature to unfold HTML comments.
// @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]';

document.addEventListener('click', inline_linked_footnote, true);
$x(footnote_links).forEach(arm_footnote_injector);
$x('//comment()').filter(hasText).forEach(showComment);

function hasText(x) {
  return x.nodeValue.replace(/<[^>]*>|^\s+|\s+$/g, '').length;
}

function showComment(c, n) {
  n = String.fromCharCode(n + 'a'.charCodeAt());

  var s = document.createElement('span');
  var a = document.createElement('a');
  s.style.color = '#777';
  a.style.color = '#444';

  a.name = 'comm-'+ n;
  a.href = '#'+ a.name;
  a.textContent = n;
  a.addEventListener('click', expandComment, true);

  s.appendChild(document.createTextNode(' <'));
  s.appendChild(a);
  s.appendChild(document.createTextNode('> '));
  c.parentNode.insertBefore(s, c);
}

function expandComment(e) {
  e.stopPropagation();
  e.preventDefault();
  var me = e.target;
  var lt = me.previousSibling; lt.nodeValue = ' <!-- ';
  var gt = me.nextSibling; gt.nodeValue = ' --> ';
  var rc = me.parentNode.nextSibling;
  var rp = me.parentNode;
  var span = document.createElement('span');
  //span.innerHTML = ': '+ rc.nodeValue;
  //rp.insertBefore(span, gt);
  //me.removeEventListener('click', expandComment, true);
  span.innerHTML = rc.nodeValue;
  rp.replaceChild(span, me);
}

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 );
  }
}