/* -*-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
