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