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