Make Apache Directory Listing Prettier





Make Apache Directory Listing Prettier

Enhance Apache's autogenerated directory listing pages with semantic, accessible tables.

Have you ever visited a page to find nothing but a plain list of files? If a folder has no default web page, the Apache web server autogenerates a directory listing with clickable filenames. Nothing fancy, but it works, so why complain? Because we can do better! This hack takes the raw data presented in Apache directory listings and replaces the entire page with a prettier, more accessible, more functional version.

The Code

This user script runs on all pages. Of course, not all pages are Apache directory listings, so the first thing the script does is check for some common signs that this page is a directory listing. Unfortunately, there is no foolproof way to tell; recent versions of Apache add a <meta> element in the <head> of the page to say that the page was autogenerated by Apache, but earlier versions of Apache did not do this. The script checks for three things:

  • The title of the page starts with "Index of /".

  • The body of the page contains a <pre> element. Apache uses this to display the plain directory listing.

  • The body of the page contains links with query parameters. Apache uses these for the column headers. Clicking a column header link re-sorts the directory listing by name, modification date, or size.

If all three of these conditions are met, the script assumes the page is an Apache directory listing, and proceeds to parse the preformatted text to extract the name, modification date, and size of each file. It constructs a table (using an actual <table> element—what a concept) and styles alternating rows with a light-gray background.

Save the following user script as betterdir.user.js:


	// ==UserScript==

	// @name BetterDir

	// @namespace http://diveintomark.org/projects/greasemonkey/

	// @description make Apache 1.3-style directory listings prettier

	// @include *

	// ==/UserScript==



	function addGlobalStyle(css) {

		var elmHead, elmStyle;	

		elmHead = document.getElementsByTagName('head')[0];



		if (!elmHead) { return; }

		elmStyle = document.createElement('style');

		elmStyle.type = 'text/css';

		elmStyle.innerHTML = css;

		elmHead.appendChild(elmStyle);

	}



	// if page title does not start with "Index of /", bail

	if (!(/^Index of \//.test(document.title))) { return; }



	// If we can't find the PRE element, this is either

	// not a directory listing at all, or it's an

	// Apache 2.x listing with fancy table output enabled

	var arPre = document.getElementsByTagName('pre');

	if (!arPre.length) { return; }

	var elmPre = arPre[0];



	// find the column headers, or bail

	var snapHeaders = document.evaluate(

		"//a[contains(@href, '?')]",

		document,

		null,

		XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,

		null);

	if (!snapHeaders.snapshotLength) { return; }



	// Tables aren't evil, they're just supposed to be used for tabular data.

	// This is tabular data, so let's make a TABLE element

	var elmTable = document.createElement('table');

	// give the table a summary, for accessibility

	elmTable.setAttribute('summary', 'Directory listing');

	var elmCaption = document.createElement('caption');

	// the "title" of the table should go in a CAPTION element

	// inside the TABLE element, for semantic purity

	elmCaption.textContent = document.evaluate("//head/title",

		document, null, XPathResult.FIRST_ORDERED_NODE_TYPE,

		null).singleNodeValue.textContent;

	elmTable.appendChild(elmCaption);



	var elmTR0 = document.createElement('tr');

	var iNumHeaders = 0;

	for (var i = 0; i < snapHeaders.snapshotLength; i++) {

		var elmHeader = snapHeaders.snapshotItem(i);

		// column headers go into TH elements, for accessibility

		var elmTH = document.createElement('th');

		var elmLink = document.createElement('a');

		elmLink.href = elmHeader.href;

		elmLink.innerHTML = elmHeader.innerHTML;

		// give each of the column header links a title,

		// to explain what will happen when you click on them

		elmLink.title = "Sort by " + elmHeader.innerHTML.toLowerCase( );



		elmTH.appendChild(elmLink);

		elmTR0.appendChild(elmTH);

		iNumHeaders++;

	}

	elmTable.appendChild(elmTR0);



	var sPreText = elmPre.innerHTML;

	if (/<hr/.test(sPreText)) {

		sPreText = sPreText.split(/<hr.*?>/)[1];

	}

	var arRows = sPreText.split(/\n/);

	var nRows = arRows.length;

	var bOdd = true;

	for (var i = 0; i < nRows; i++) {

		var sRow = arRows[i];

		sRow = sRow.replace(/^\s*|\s*$/g, '');

		if (!sRow) { continue; }

		if (/\<hr/.test(sRow)) { continue; }

		var arTemp = sRow.split(/<\/a>/);

		var sLink = arTemp[0] + '</a>';

		if (/<img/.test(sLink)) {

			sLink = sLink.split(/<img.*?>/)[1];

		}

		sRestOfLine = arTemp[1];

		arRestOfCols = sRestOfLine.split(/\s+/);



		var elmTR = document.createElement('tr');

		var elmTD = document.createElement('td');

		elmTD.innerHTML = sLink;

		elmTR.appendChild(elmTD);



		var iNumColumns = arRestOfCols.length;

		var bRightAlign = false;

		for (var j = 1 /* really */; j < iNumColumns; j++) {

			var sColumn = arRestOfCols[j];

			if (/\d\d:\d\d/.test(sColumn)) {

				elmTD.innerHTML += ' ' + sColumn;

			} else {

				elmTD = document.createElement('td');

				elmTD.innerHTML = arRestOfCols[j];

				if (bRightAlign) {

				elmTD.setAttribute('class', 'flushright');

				}

				elmTR.appendChild(elmTD);

			}

			bRightAlign = true;

		}

		while (iNumColumns <= iNumHeaders) {

			elmTR.appendChild(document.createElement('td'));

			iNumColumns++;

	

		}



		// zebra-stripe table rows, from

		// http://www.alistapart.com/articles/zebratables/

		// and http://www.alistapart.com/articles/tableruler/

		elmTR.style.backgroundColor = bOdd ? '#eee' : '#fff';

		elmTR.addEventListener('mouseover', function( ) {

			this.className = 'ruled';

		}, true);

		elmTR.addEventListener('mouseout', function( ) {

			this.className = '';

		}, true);

		elmTable.appendChild(elmTR);



		bOdd = !bOdd;

	}



	// copy address footer -- probably a much easier way to do this,

	// but it's not always there (depends on httpd.conf options)

	var sFooter = document.getElementsByTagName('address')[0];

	var elmFooter = null;

	if (sFooter) {

		elmFooter = document.createElement('address');

		elmFooter.innerHTML = sFooter.innerHTML;

	}



	window.addEventListener('load',

		function( ) {

			document.body.innerHTML = '';

			document.body.appendChild(elmTable);

			if (elmFooter) {

				document.body.appendChild(elmFooter);

			}

		},

		true);



	// now that everything is semantic and accessible,

	// make it a little prettier too

	addGlobalStyle(

	'table {' +

	' border-collapse: collapse;' +

	' border-spacing: 0px 5px;' +

	' margin-top: 1em;' +

	' width: 100%;' +

	'}' +

	'caption {' +

	' text-align: left;' +

	' font-weight: bold;' +

	' font-size: 180%;' +

	' font-family: Optima, Verdana, sans-serif;' +

	

	'}' +

	'tr {' +

	'  padding-bottom: 5px;' +

	'}' +

	'td, th {' +

	' font-size: medium;' +

	' text-align: right;' +

	'}' +

	'th {' +

	' font-family: Optima, Verdana, sans-serif;' +

	' padding-right: 10px;' +

	' padding-bottom: 0.5em;' +

	'}' +

	'th:first-child {' +

	' padding-left: 20px;' +

	'}' +

	'td:first-child,' +

	'td:last-child,' +

	'th:first-child,' +

	'th:last-child {' +

	' text-align: left;' +

	'}' +

	'td {' +

	' font-family: monospace;' +

	' border-bottom: 1px solid silver;' +

	' padding: 3px 10px 3px 20px;' +

	' border-bottom: 1px dotted #003399;' +

	'}' +

	'td a {' +

	' text-decoration: none;' +

	'}' +

	'tr.ruled {' +

	' background-color: #88eecc ! important;' +

	'}' +

	'address {' +

	' margin-top: 1em;' +

	' font-style: italic;' +

	' font-family: Optima, Verdana, sans-serif;' +

	' font-size: small;' +

	' background-color: transparent;' +

	' color: silver;' +

	'}');


Running the Hack

Before installing the user script, go to http://diveintomark.org/projects/greasemonkey/. There is no default page for this directory, so Apache automatically generates a plain-text directory listing, as shown in Figure.

Now, install the user script (Tools Install This User Script) and refresh http://diveintomark.org/projects/greasemonkey/. The user script replaces the plain directory listing with an enhanced version, which contains a real table with alternating rows shaded, as shown in Figure.

Plain Apache directory listing


When you hover over a file, the entire row is highlighted, as shown in Figure.

Also, when you hover over one of the column headers, you will see a tool tip explaining that you can click to sort the directory listing, as shown in Figure.

I've probably seen thousands of autogenerated directory listings, and it wasn't until I wrote this hack that I realized that you could click a column header to change the sort order. Usability matters!

Enhanced Apache directory listing


Row highlighting


Column sorting



     Python   SQL   Java   php   Perl 
     game development   web development   internet   *nix   graphics   hardware 
     telecommunications   C++ 
     Flash   Active Directory   Windows