By Sean M. Burke
—
Last update
Jan 13, 2006
—
Installed
812 times.
Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)
/* -*-mode:JavaScript;coding:latin-1;-*- Time-stamp: "2006-02-09 03:54:54 AST"
##### This is a Greasemonkey user script.
##### To use it, you need Greasemonkey first: http://greasemonkey.mozdev.org/
*/
// ==UserScript==
// @name Directory_Index_UI
// @namespace http://interglacial.com/~sburke/pub/
// @description adds helpful UI features to server-generated directory index pages
// @version 0.0.4
// @include */
// @include */?*
// @include file://*
// @exclude file://*.html
// @exclude file://*.htm
// @exclude file://*.shtml
// @author sburke@cpan.org
// ==/UserScript==
/*
~~* "Directory Index UI" *~~
Summary:
This userscript turns directory indexes like this:
http://interglacial.com/~sburke/pub/GreaseMonkey/apache_plain.png
into this:
http://interglacial.com/~sburke/pub/GreaseMonkey/apache_fancy.png
Full description:
Server-generated directory index pages (like at haidalanguage.org/date/ )
lack some features that I want. So I wrote this GM extension to make
it the way I want! Notably:
* The text string "Index of /foo/bar/baz" is replaced by
"[foo] [bar] baz", where "foo" links to the directory "foo" and "bar"
links to the directory "bar". (The "baz" isn't a link, because you're
already there.)
* Alt-U is made to go to the parent directory. (Accesskeys are fun!)
* Instead of just headings "Name" "Last modified" "Size" "Description",
there's proper tabs for sorting the various ways, with the one for
the current mode greyed out.
* The actual directory items get colorbars on alternate lines (like
with ledger paper). This is to help readability.
* This userscript works its magic on directory indexes as generated by
Apache 1 and Apache 2, and even manages to partly work for some other
servers' indexes. If it doesn't know how to deal with an index
format, it leaves it alone.
* When the directory index truncates the visible name of a file
(like "haida_cal_2004_to_20..>" from "haida_cal_2004_to_2009.rtf"), we put
the full filename on the link's "title" attribute, which shows up on a
tooltip when you mouse over it.
* Also, it's pretty. Pretty pretty pretty. And configurable in various
ways -- see the code below.
*/
// Configurables! ----------------------------------------------------------
var Max_Items_To_Colorbar = 500;
var Turn_Underscores_To_Spaces = true;
var Keep_Slashes = false;
var Keep_Index_Of = false;
var Keep_Old_Headers = false;
var Body_style =
"margin: 0; border-bottom: 6px #bdf5f5 solid;"
;
var Heading_style =
"font-size: inherit !important; font-size: 120% !important; " +
"background-color: #c2fafa; color: black; font-weight: normal;" +
"padding: 6px 8px 2px 3px; margin: 0; line-height: 145%;" +
"border-top: 6px #a2dada solid;" +
"border-bottom: 6px #a2dada solid;" +
"-moz-border-top-colors: #a2dada #a7dfdf #ade5e5 #b2eaea #b7efef #bdf5f5;" +
"-moz-border-bottom-colors: #ffffff #f5fefe #ebfdfd #e1fdfd #d6fcfc #ccfbfb #c2fafa;"
;
var Heading_link_style =
"background-color: #b0d0ff; color: black;" +
"text-decoration: none !important;" +
"border: 3px #90c0df solid; -moz-border-radius: 23px 0px; padding: 0 1px;"
;
var Heading_lastbit_style =
"text-decoration: none !important; background: #d5f2ff; " +
"border: 3px #70d0ff solid; -moz-border-radius: 23px 0px; padding: 0 1px;"
;
var Sorty_Tabs_Stylesheet =
"pre { margin: 0 0 0 10px; padding: 0;}\n"
+".petallic { margin: 0em; padding: 0;}\n"
+".tabular { display: inline; padding-right: 3px;}\n"
+".rowal { display: table-row; }\n"
+".cellular { display: table-cell; text-align: center; "
+" background-color: #b0ffd0; border: 2px black solid; width: auto; "
+" font-size: 80%; line-height: 78%; padding: 0px 1px 2px 1px; "
+" border: 3px #90dfc0 solid; -moz-border-radius: 23px 0px;}\n"
+".calready { background-color:#f0f0f0; border-color:#d0d0d0; color:#888;}\n"
+".linky { color:#888 !important; text-decoration: none !important; }\n"
;
var DEBUG = 0; // 0 for no progress messages and comments, 100 for all.
// -100 will quiet even the rare error messages.
//-- End of configurables --------------------------------------------------
//
var Apache_1_Sorty_RE = /^\?[NMSD]=[AD]$/;
var Apache_1_Sorties = [
'?N=A', '?N=D', // Name A-Z, Z-A
'?M=A', '?M=D', // Modtime old->new, new->old
'?S=A', '?S=D', // Size small->big, big->small
'?D=A', '?D=D' // Desc A-Z, Z-A
];
var Apache_2_Sorty_RE = /^\?C=[NMSD];O=[AD]$/;
var Apache_2_Sorties = [
'?C=N;O=A', '?C=N;O=D', // Name A-Z, Z-A
'?C=M;O=A', '?C=M;O=D', // Modtime old->new, new->old
'?C=S;O=A', '?C=S;O=D', // Size small->big, big->small
'?C=D;O=A', '?C=D;O=D' // Desc A-Z, Z-A
];
var Sorties; // pointed to one of the above sorties later
var Colheaders = [];
var Sortypetals = [
["A","Z", "Sort with 'A-' names first",
"Sort with 'Z-' names first", '1.3em' ],
["old","new", "Sort with oldest first",
"Sort with newest first", '13em' ],
["small","\xa0big\xa0", "Sort with smallest first",
"Sort with biggest first", '3.6em' ],
["A", "Z", "Sort with 'A-' descriptions first",
"Sort with 'Z-' descriptions first", '2em' ]
];
var Bar_Colors = [
"#ffffff", "#f8e8e8"
//,"#ffffff", "#ffe0e0"
//,"#ffffff", "#e0ffe0"
//,"#ffffff", "#e0e0ff"
];
var B = document.body;
if ( document.documentElement
&& document.documentElement.tagName == "HTML"
&& document.contentType == "text/html"
&& B // Basic sanity
) { trace(11, "Starting up."); run(); }
// - - - - - - - - -
function trace (level,msg) {
if(DEBUG >= level) GM_log(msg);
var e = new Error(msg); // just in case we're doing "throw trace(...)".
e.smb_debuglevel = level;
return e;
}
var Heading, Pre;
function run () {
var e;
try {
find_heading_el();
revamp_heading();
find_pre_el();
revamp_sorters();
add_colorbars();
trace(11, "Ending happily.");
} catch (e) {
if(!e) { e = new Error("NullError!?!?"); }
if( "smb_debuglevel" in e ) {
trace(11, "Ending with an exception.");
} else {
GM_log( (e.smb_debuglevel || "??") + " oboy!" );
throw e;
}
}
return;
}
// - - - - - - - - -
var Current_sort_mode, Spacer;
function revamp_sorters () {
figure_out_current_sort_mode();
insert_our_stylesheet();
var bunch = document.createElement("p");
bunch.setAttribute('class','petallic');
Pre.parentNode.insertBefore(bunch, Pre);
var bunch2 = graft(bunch, ['code']); // That's so that our "ems" are at least
// related to the (presumably fixed) spacing in the Pre area. Otherwise
// we get ems based on the browser's proportional font, which may be
// completely unrelated to the width of chars in the Pre area.
bunch = bunch2;
if( Spacer ) bunch.appendChild( Spacer.cloneNode(true) );
for(var i = 0; i < Sortypetals.length; i++) {
var to1 = Sorties[i*2], to2 = Sorties[i*2 +1], p = Sortypetals[i];
linky(bunch, p[0], p[1], p[2], p[4], to1);
linky(bunch, p[1], p[0], p[3], 0, to2);
}
if(!Keep_Old_Headers) {
for(i = 0; i < Colheaders.length; i++) { // drop the old colheaders
Pre.removeChild( Colheaders[i] );
}
}
return;
}
function linky (parent, topname, bottomname, label, spacing, to) {
// CSS insanity! CSS insanity! CSS insanity!
trace(22, "linking to " + to);
var dead = (to == Current_sort_mode);
var cell = ['span', {'class':"cellular"}];
var there; // where we'll put the text
if(dead) {
there = cell;
there[1]['class'] += " calready";
there[1].title = label + " (Current mode)";
} else {
// Normal case
there = ['a', {'href':to, 'title':label, 'class':'linky' } ];
cell.push(there);
}
there.push(topname || "X??", ['br'], bottomname || "Y??");
graft(parent,
[ 'span', {'style':("padding-left:" + spacing),'class':"tabular"},
[ 'span', {'class':"rowal"}, cell ]]);
return;
}
function add_colorbars () {
if(!Spacer) {
trace(5, "Skipping adding colorbars to spacerless pre.");
return;
}
var colors_count = Bar_Colors.length;
var prech = Pre.childNodes;
var prech_count = Pre.childNodes.length;
var nodename, newel, thisel;
var line_count = -1;
for(var i = 1; i < prech_count; i++) {
thisel = prech.item(i);
if(!thisel.nodeName) throw("NOTHING: " + thisel + "?!");
nodename = (thisel).nodeName;
if(nodename == "#text") {
if(line_count == -1) continue;
newel = document.createElement('span');
Pre.replaceChild(newel, thisel);
newel.appendChild(thisel);
newel.style.backgroundColor = Bar_Colors[line_count];
// because we need something to apply a style to!
} else if(nodename == 'A') {
if(line_count > -1) thisel.style.backgroundColor = Bar_Colors[line_count];
// And also see if we need to make up for truncation.
var href = thisel.getAttribute('href') || '';
if(href && href.length > 22 ) {
var texty = thisel.firstChild.nodeValue.toString();
if( texty.lastIndexOf('>') == (texty.length - 1) ) {
thisel.setAttribute( 'title', decodeURI( href ) );
}
}
} else if(nodename == 'IMG') { // signals a new line
if(++line_count >= colors_count) line_count = 0;
//trace(0, "line_count: " + line_count.toString());
}
}
return;
}
function insert_our_stylesheet () {
var style = document.createElement("style");
style.setAttribute("type", 'text/css');
style.setAttribute("media", 'screen');
style.appendChild( document.createTextNode( Sorty_Tabs_Stylesheet ) );
B.insertBefore(style, B.firstChild);
return;
}
function figure_out_current_sort_mode () {
var current_try = document.location.search;
Current_sort_mode = Sorties[0];
if( current_try == null || current_try == "?" || current_try == "") {
trace(19, "Sort-mode defaulting to \xab" + Current_sort_mode + "\xbb.");
return;
}
for(var i = 0; i < Sorties.length; i++) {
if(current_try == Sorties[i]) {
Current_sort_mode = current_try;
trace(15, "Recognizing sort-mode \xab" + Current_sort_mode + "\xbb.");
return;
}
}
trace(14, "Current sort mode is \xab" + current_try +
"\xbb, which is unrecognized. Defaulting to \xab" +
Sorties[0] + "\xbb.");
return;
}
// - - - - - - - - -
function nodenames_of (nry) { // array of nodes
var out = [];
for(var i = 0; i < nry.length; i++) { out[i] = (nry[i]||'').nodeName || '~' }
return out.join(",");
}
function first_n_child_nodes (el, n) { // n is 1-indexed.
if(!(el && el.childNodes)) return [];
var c = el.childNodes, them = [];
if(n == null) n = Infinity;
for(var i = 0; ((i < n) && (i < c.length)); i++) {
them[i] = c.item(i);
//GM_log( "Nodename: " + c.item(i).nodeName );
}
return them;
}
function nodelist_matches_template (nodes, match_into, template) {
if(template == "") return( (nodes.length == 0) ? [] : false );
var tsize = template.split(',').length;
if(tsize > nodes.length) return false;
var sl = nodes.slice(0,tsize);
if( nodenames_of(sl) != template ) {
//GM_log("Doesn't match:\n "+nodenames_of(sl) + "\n != "+ template);
return false;
}
trace(12, "Matches template: " + template);
// Empty it out and pour nodes into it:
match_into.splice(0);
for(var i = 0; i < tsize; i++) { match_into[i] = sl[i] }
return true;
}
function find_pre_el () {
var n = Heading.nextSibling;
if(!n) throw trace(20, "Nothing after h1");
if(n.nodeName == "#text") n = n.nextSibling;
if(!n) throw trace(20, "Nothing after h1 etc");
if(n.nodeName != "PRE") throw trace(20,"First element after h1 isn't a pre.");
var cnl = n.childNodes;
if( cnl.length > Max_Items_To_Colorbar * 4 )
throw trace(-1,"Too many items in " + document.location
+ " -- spiffing it up would take too long.");
if(!(cnl && cnl.length >= 11)) // todo: still right for empty iconless pgs?
throw trace(20,"pre.childNodes.length too short");
trace(20, "Found the pre element.");
var cn = first_n_child_nodes(n,12);
if( nodelist_matches_template(cn,Colheaders,
"IMG,#text,A,#text,A,#text,A,#text,A,#text,HR,#text")) {
trace(12, "The Pre has a conventional img-a-a-etc structure with desklink.");
Spacer = Colheaders[0];
link1i = 2;
} else if( nodelist_matches_template(cn,Colheaders,
"IMG,#text,A,#text,A,#text,A,#text,A")) {
trace(12, "Pre is conventional img-a-a-etc without desklink.");
Spacer = Colheaders[0];
link1i = 2;
} else if( nodelist_matches_template(cn,Colheaders,
"A,#text,A,#text,A,#text,A,#text,HR")) {
trace(12, "The Pre has an iconless structure with desclink.");
Spacer = null;
link1i = 0;
} else if( nodelist_matches_template(cn,Colheaders,
"A,#text,A,#text,A,#text,A")) {
trace(12, "The Pre has an iconless structure without desclink.");
Spacer = null;
link1i = 0;
} else {
throw trace(5, "This is no Apache index page, as the pre's child list "
+ "starts out like this: ["
+ nodenames_of(cn) + "]");
}
// Now let's figure out whether this page is from Apache 1 or 2.
// At least one of these two links should reveal it.
var c1 = Colheaders[link1i ].getAttribute('href'),
c2 = Colheaders[link1i + 2].getAttribute('href');
if( Apache_2_Sorty_RE.test(c2) || Apache_2_Sorty_RE.test(c1) ) {
Sorties = Apache_2_Sorties;
trace(12, "It's an Apache2 index page.");
} else if( Apache_1_Sorty_RE.test(c2) || Apache_1_Sorty_RE.test(c1) ) {
Sorties = Apache_1_Sorties;
trace(12, "It's an Apache1 index page.");
} else {
throw trace(5,
"It's not an Apache 1 or 2 index page. Lookit these weird links: "
+ c1 + " and " + c2);
}
Pre = n;
// Fixing the " " alt problem; it'd shrink to one space outside the pre!
if( Spacer ) {
var alt = Spacer.getAttribute("alt");
if(alt && alt == " ") {
Spacer.setAttribute('alt', "\x80\x80\x80\x80\x80");
}
}
return;
}
// - - - - - - - - -
function revamp_heading () {
replace_heading_with( scan_heading() )
if(Heading_style) Heading.setAttribute('style', Heading_style);
if(Body_style) B .setAttribute('style', Body_style);
return;
}
// - - - - - - - - -
function replace_heading_with (bits) {
if(!bits) throw trace(1, "No bits-list?");
if(!bits.length) throw trace( 1, "No bits?" );
Heading.removeChild(Heading.firstChild);
var el; //scratch
for(var i = 0; i < bits.length; i++) {
thisbit = bits[i];
if(!thisbit.push) throw trace(-1, "WHAT Nonarray?! " + thisbit.toSource());
if(i) graft(Heading, ['span', " "]).style.fontSize = '9px';
var text = proc_underscores( thisbit[0] ) ;
if(thisbit.length == 2) {
trace(11, i.toString() + "\xAC Linky \xAB" + thisbit[0]
+ "\xBB = \xAB" + thisbit[1] + "\xBB");
var atts = {'href': thisbit[1]};
if(Heading_link_style) atts.style = Heading_link_style;
if(thisbit[1] == "../" ) {
atts.accesskey = 'u';
atts.title = 'alt-u: up to parent directory';
}
graft( Heading, ['a', atts, text]);
} else if(thisbit.length == 1) {
trace(11, i.toString() + "\xAC Texty \xAB" + thisbit[0] + "\xBB");
graft( Heading,
(Heading_lastbit_style && (i == bits.length - 1)) ?
['span', {style:Heading_lastbit_style}, text] : text
);
} else {
throw trace(-1, "WHAT Empty?! " + thisbit.toSource());
}
}
return;
}
function scan_heading () {
var m;
var t = Heading.firstChild;
var tdata = t.data.toString();
m = tdata.match( /^(Index of )(file:)?(\/.*)$/ );
if(!m) throw trace(10, "Index string is crazy: \xAB" + tdata + "\xBB");
var label = m[1], prefix = m[2], path = m[3];
trace(10, "Okay, path is \xAB" + path + "\xBB");
var scanner = /([\/]+)|([^\/]+)/g;
var bits = []; // nodes to be created
var linky_bits = []; // runs backwards
if(Keep_Index_Of) bits.push( [label] );
var slashcount = 0;
while(1) {
m = scanner.exec(path);
if(!m) break;
if(m[1]) { // slashes
++slashcount;
if(slashcount == 1) {
// else it's an initial slash, and we keep it!
if(prefix) { m = [ "SPORK", prefix + m[1] ] }
// And fall thru to the linky part
} else {
var text = [];
trace(15, "Slashy bit \xAB" + m[1] + "\xBB!\n");
if(Keep_Slashes) bits.push( [m[1]] );
continue;
}
}
var thisbit = [ m[1] || m[2] ];
trace(15, "Nonslashy bit \xAB" + thisbit[0] + "\xBB!\n");
bits.push(thisbit)
linky_bits.unshift(thisbit);
}
linky_bits.shift(); // make the last thing just a text thingy
var urlup = "", up = "../";
var i;
for(i = 0; i < linky_bits.length; i++) {
thisbit = linky_bits[i];
urlup += up;
thisbit.push( urlup );
trace(15, i.toString() + "\xBB" + thisbit.toString());
}
return bits;
}
function find_heading_el () {
var n = B.firstChild;
if(!n) throw trace(20, "Nothing in body!");
if(n.nodeName == "#text") n = n.nextSibling;
if(!n) throw trace(20, "Nothing in body etc!");
if(n.nodeName != "H1") {
if(n.nodeName == "TABLE"
&& document.firstChild.nodeType == document.DOCUMENT_TYPE_NODE
&& document.firstChild.systemId
== "http://www.w3.org/TR/REC-html40/loose.dtd"
) {
return find_heading_el_maybe_apache_13(n);
} else {
throw trace(20,"First element isn't an h1 or a table.");
}
}
if(!(n.childNodes && n.childNodes.length == 1))
throw trace(20,"h1.childNodes.length != 1");
var t = n.firstChild;
if(t.nodeName != "#text") throw trace(20,
"h1's child isn't a text node, it's a " + t.nodeName + ".");
if( (t.data || '') .indexOf('Index of ') != 0 )
throw trace(20, "h1's text doesn't start with 'Index of'.");
trace(20, "Found the h1 element.");
Heading = n;
return;
}
function find_heading_el_maybe_apache_13 (table) {
//I hate you, Milkman Dan
trace(20, "Hm, starts out with a table. Maybe it's an Apache 1.3.suck index.");
var n = table;
if(
n.childNodes.length == 1 && (n = n.childNodes[0]).nodeName == "TBODY"
&& n.childNodes.length == 1 && (n = n.childNodes[0]).nodeName == "TR"
&& n.childNodes.length == 1 && (n = n.childNodes[0]).nodeName == "TD"
&& n.childNodes.length == 3 && (n = n.childNodes[1]).nodeName == "FONT"
&& n.childNodes.length == 2 && (n = n.childNodes[1]).nodeName == "B"
&& n.childNodes.length == 1 && (n = n.childNodes[0]).nodeName == "#text"
) {
trace(20, "It looks like an Apache 1.3.suck index."); // and fall thru
} else {
throw trace(20, "Doesn't look like an Apache 1.3.suck index.");
}
var text = (n.data || '').toString() || '';
if( text.indexOf('Index of ') != 0 )
throw trace(20, "philo-H1's text doesn't start with 'Index of'.");
trace(20, "Found the philo-H1 element's content. Making it a real H1.");
Heading = document.createElement("h1");
Heading.appendChild( document.createTextNode( text ) );
B.insertBefore(Heading, table);
B.removeChild( table );
return;
}
// - - - - - - - - -
function proc_underscores (s) {
return(Turn_Underscores_To_Spaces ? s.replace( /_/g, ' ' ) : s);
}
// - - - - - - - - -
// A general-utility function for node-making. It beats dealing with the DOM!
function graft (parent, t) {
// graft( somenode, [ "I like ", ['em', { 'class':"stuff" },"stuff"], " oboy!"] )
//if(!doc) doc = parent.ownerDocument ? parent.ownerDocument : document;
var doc = (doc || parent.ownerDocument || document);
var e;
if(t == undefined) {
if(parent == undefined) throw trace(-1,"Can't graft an undefined value");
t = parent;
parent = id('stage');
} else if(t.constructor == String) {
e = doc.createTextNode( t );
} else if(t.length == 0) {
e = doc.createElement( "span" );
e.setAttribute( "class", "fromEmptyLOL" );
} else {
for(var i = 0; i < t.length; i++) {
if( i == 0 && t[i].constructor == String ) {
var snared = t[i].match( /^([a-z][a-z0-9]*)$/i );
if( snared ) {
e = doc.createElement( snared[1] );
continue;
}
// Otherwise:
e = doc.createElement( "span" );
e.setAttribute( "class", "namelessFromLOL" );
}
if( t[i] == undefined ) {
throw trace(-1,"Can't graft an undefined value in a list!");
} else if( t[i].constructor == String || t[i].constructor == Array ) {
graft( e, t[i], doc );
} else if( t[i].constructor == Number ) {
graft( e, t[i].toString(), doc );
} else if( t[i].constructor == Object ) {
// turn this hash's properties:values into attributes of this element
for(var k in t[i]) e.setAttribute( k, t[i][k] );
} else {
throw trace(-1, "Object " + t[i] + " is inscrutable as an graft arglet." );
}
}
}
parent.appendChild( e );
return e; // returns the created element
}
// All you JavaScript goons, read http://interglacial.com/hoj/ !
// End