Greasemonkey Hacks/Beautifying the Web

From WikiContent

Jump to: navigation, search
Greasemonkey Hacks


Contents

Hacks 21–28: Introduction

Graphic designers have long decried the constraints of the Web. Differences across browsers and platforms destroy their carefully designed layouts. Everything takes too long to load, destroying their hopes for the user experience. And don't even get them started about fonts.

But all is not lost. The Web might look grungy and minimalist compared to a well-laid-out magazine or book, but that doesn't mean we can't take a few steps in the right direction. The hacks in this chapter focus on making the Web a kinder, gentler, more visually appealing place to visit. Well, more visually appealing anyway. Kindness and gentleness: you're on your own for those.

Banish the Scourge of Arial

Make the Web a typographically better place.

The Arial font is the bane of typographical snobs everywhere. Originally conceived as a cheap clone of Helvetica (due to licensing fees), Arial was adopted by Microsoft in Windows 3.1 and has since taken over the world. Firefox uses Arial as one of the default fonts for web pages that don't specify a default. Despite the rich capabilities for specifying multiple fallback fonts in modern browsers, Arial continues to dominate typography on the Web.

The first thing I do when I reinstall Windows (and the first thing you should do before running this hack) is change the default font in Firefox. Under Windows, select Tools → Options to open the preferences dialog. In the General pane, click the Fonts & Colors button and change the Sans-Serif font from Arial to something else. I'm partial to Helvetica or Verdana on Windows, and Mac OS X comes with a handsome font called Optima. But almost any choice is better than Arial.

Tip

Read more about the history of Arial at http://www.msstudio.com/articles.html.

The Code

This user script runs on all pages. It iterates through all the elements on the page and gets the element's style (using getComputedStyle), then removes Arial from the list of fonts for that element.

Tip

You might think that you could simply access an element's style by checking its style attribute. But this attribute includes only inline styles defined in a style attribute on the original page. It doesn't include styles applied by external stylesheets. To get an element's actual style, you need to call the getComputedStyle function.

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

	// ==UserScript==
	// @name		Scourge of Arial
	// @namespace	http://diveintomark.org/projects/greasemonkey/
	// @description banish the scourge of Arial
	// @include		*
	// ==/UserScript==

	var arElements = document.getElementsByTagName('*');
	for (var i = arElements.length - 1; i >= 0; i--) {
		var elm = arElements[i];
		var style = getComputedStyle(elm, '');
		elm.style.fontFamily = style.fontFamily.replace(/arial/i, '');
	}

Running the Hack

As I mentioned before, the first thing you should do is change your default sans-serif font from Arial to something else. If you don't do this, this hack won't have any effect, because an element with a font declaration of Arial, sans-serif will be changed to sans-serif and use the default font, but the default font would still be Arial!

Before installing the user script, go to http://www.google.com, which uses Arial for all the text and links on the page, as shown in Figure 3-1.

Now, install the user script (Tools → Install This User Script), and refresh http://www.google.com. You will see the text and links change to the font you defined as your default sans-serif font in the Firefox preferences dialog. I changed mine to Verdana, as shown in Figure 3-2.

Figure 3-1. Google.com with the scourge of Arial

Google.com with the scourge of Arial

Figure 3-2. Google with Verdana instead of Arial

Google with Verdana instead of Arial

Hacking the Hack

Currently, this hack only removes Arial; it doesn't replace it with anything. Web pages that define Arial as the only font will end up with no font declaration at all, and Firefox will display them with a serif font such as Times. To get around this problem, we can update the script to substitute sansserif instead of removing Arial altogether.

Change this line:

	elm.style.font defaultsfontFamily = style.fontFamily.replace(/arial/i, '');

to this:

	elm.style.fontFamily = style.fontFamily.replace(/arial/i, 'sans-serif');

Add Stripes to Data Tables

Make tables easier to read by highlighting alternate rows.

Web pages can display tables of data, like a spreadsheet. However, most web publishers don't put a lot of thought into the usability of large tables. Small improvements such as highlighting every other row can make a huge difference in readability. I honestly didn't think such a little detail would matter to me, since I have normal eyesight and don't spend a lot of time poring over reports or spreadsheets online. But the difference is amazing! I can't imagine how I ever lived without this hack.

The Code

This user script runs on all pages. It is relatively straightforward. It gets all the table rows (<tr> elements) and then loops through them to set the background color to #ddd or #fff.

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

	// ==UserScript==
	// @name		Table Stripes
	// @namespace	http://diveintomark.org/projects/greasemonkey/
	// @description shade alternating rows of data tables
	// @include		*
	// ==/UserScript==

	var arTableRows = document.getElementsByTagName('tr');
	var bHighlight = true;
	for (var i = arTableRows.length - 1; i >= 0; i--) {
		var elmRow = arTableRows[i];
		elmRow.style.backgroundColor = bHighlight ? '#ddd' : '#fff';
		elmRow.style.color = '#000';
		bHighlight = !bHighlight;
	}

Running the Hack

Before installing the user script, go to http://www.openbsd.org/3.7_packages/i386.html, which displays a large table of available packages for the Open-BSD operating system, as shown in Figure 3-3.

Figure 3-3. OpenBSD packages

OpenBSD packages

Now install the user script (Tools → Install This User Script), and refresh http://www.openbsd.org/3.7_packages/i386.html. You will see every other row is now slightly shaded, as shown in Figure 3-4. This makes it much easier to read across the table and associate the package name with its description.

Figure 3-4. OpenBSD packages, striped

OpenBSD packages, striped

Hacking the Hack

Currently, this hack shades alternating rows in any table. But many web pages use tables for layout, and this hack could seriously alter their display in unexpected and bizarre ways. To get around this, we can use XPath to operate only on tables that include table headers (<th> elements). Table headers are rarely used in layout tables; usually, you find them only in tables that actually display tabular data.

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

	// ==UserScript==
	// @name		Table data tablesadding stripesStripes
	// @namespace	http://diveintomark.org/projects/greasemonkey/
	// @description shade alternating rows of data tables
	// @include		*
	// ==/UserScript==
	
	var snapTableRows = document.evaluate("//table//th/ancestor::table//tr",
		document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
	var bHighlight = true;
	for (var i = snapTableRows.snapshotLength - 1; i >= 0; i--) {
		var elmRow = snapTableRows.snapshotItem(i);
		elmRow.style.backgroundColor = bHighlight ? '#ddd' : '#fff';
		elmRow.style.color = '#000';
		bHighlight = !bHighlight;
	}

There is another obvious candidate for striping: lists. Simply by taking the original hack and changing the first line of code to search for <li> elements instead of <tr> elements, we can highlight alternating items in ordered and unordered lists.

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

	// ==UserScript==
	// @name		List Stripes
	// @namespace	http://diveintomark.org/projects/greasemonkey/
	// @description shade alternating rows of lists
	// @include		*
	// ==/UserScript==

	var arListItems = document.getElementsByTagName('li');
	var bHighlight = true;
	for (var i = arListItems.length - 1; i >= 0; i--) {
		var elmListItem = arListItems[i];
		elmListItem.style.backgroundColor = bHighlight ? '#ddd' : '#fff';
		elmListItem.style.color = '#000';
		bHighlight = !bHighlight;
	}	

Yahoo! uses lists to display search results, so you can see this effect by searching for something in Yahoo! Web Search, as shown in Figure 3-5.

Every other search result is slightly shaded, which makes it easier to scan the page when you need to look past the first search result.

Figure 3-5. Yahoo! Search results, striped

Yahoo! Search results, striped

Straighten Smart Quotes

Convert curly quotes, apostrophes, and other fancy typographical symbols back to their ASCII equivalents.

Have you ever gone to copy a block of text from a web site and paste it into a text editor (or try to paste it into a weblog post of your own)? The text comes through, but all the apostrophes and quote marks end up as random-looking symbols. The web site uses fancy publishing software to produce smart quotes and apostrophes, but your text editor doesn't understand them. This hack dumbs down these fancy typographical symbols to their ASCII equivalents.

The Code

This user script runs on all pages. It constructs an array of fancy characters (by their Unicode representation). Then, it gets a list of all the text nodes on the page and executes a search-and-replace on each node to convert each fancy character to a plain-text equivalent.

Tip

Learn more about Unicode at http://www.unicode.org.

In JavaScript, the replace method takes a regular expression object as its first parameter. For performance reasons, we build all our regular expressions first, and then reuse them every time through the loop. If we had used the inline regular expression syntax, Firefox would need to rebuild each regular expression object every time through the loop—a significant performance drain on large pages!

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

	// ==UserScript==
	// @name		smart quotesDumbQuotes
	// @namespace	http://diveintomark.org/projects/greasemonkey/
	// @description straighten curly quotes and apostrophes
	// @include		*
	// ==/UserScript==

	var arReplacements = {
		"\xa0": " ",
		"\xa9": "(c)",
		"\xae": "(r)",
		"\xb7": "*",
		"\u2018": "'",
		"\u2019": "'",
		"\u201c": '"',
		"\u201d": '"',
		"\u2026": "…",
		"\u2002": " ",
		"\u2003": " ",
		"\u2009": " ",
		"\u2013": "-",
		"\u2014": "--",
		"\u2122": "(tm)"};
   var arRegex = new Array();
   for (var sKey in arReplacements) {
	   arRegex[sKey] = new RegExp(sKey, 'g');
   }

   var snapTextNodes = document.evaluate("//text()[" +
	   "not(ancestor::script) and not(ancestor::style)]",
	   document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
   for (var i = snapTextNodes.snapshotLength - 1; i >= 0; i--) {
	  var elmTextNode = snapTextNodes.snapshotItem(i);
	  var sText = elmTextNode.data;
	  for (var sKey in arReplacements) {
		  sText = sText.replace(arRegex[sKey], arReplacements[sKey]);
	  }
	  elmTextNode.data = sText;
  }

Running the Hack

Before installing the user script, go to http://www.alistapart.com/articles/emen/. As shown in Figure 3-6, the fourth paragraph reads "But the larger problem is, now that they're available, almost no one publishing on the web today knows how to use them—or often even knows of their existence." There are two fancy characters here: the apostrophe in the word they're and the dash between them and or.

Figure 3-6. Web page with fancy topography

Web page with fancy topography

Now, install the user script (Tools → Install This User Script) and refresh the page at http://www.alistapart.com/articles/emen/. As shown in Figure 3-7, the two fancy characters have been replaced with their ASCII equivalents. The apostrophe has been converted to a straight apostrophe, and the dash has been replaced with two hyphen characters.

Figure 3-7. Web page with plain topography

Web page with plain topography

Although this hack currently focuses on typographical symbols, there is nothing typography-specific about it. It's just a generic script that does global search-and-replace on the text of a web page. By altering the arReplacements array, you can replace any character, word, or phrase with anything else, on any web page. Obviously, this can lead to all sorts of mischief, if you were so inclined. I will leave this one up to your imagination….

Convert Graphical Smileys to Text

Are you tired of little smiley icons infesting web pages and discussion forums? Convert them back to text!

I originally wrote this hack in response to a joke. Someone on the Greasemonkey mailing list announced that he had developed a user script to convert ASCII smileys such as :-) to their graphical equivalents. Someone else responded, wondering how long it would take for someone to do the reverse: convert graphical smileys back to text.

For the record, it took me about 20 minutes. Most of the time was spent researching publishing software that autogenerated graphical smileys and compiling a comprehensive list of variations.

Tip

The list of smileys in this hack was taken from http://www.phpbb.com/admin_demo/admin_smilies.htm. PHPBB is a popular application for hosting web-based discussion forums.

The Code

This user script runs on all pages. It relies on the fact that most graphical smileys are autogenerated by web publishing software, and the software puts the text equivalent of the smiley in the image's alt attribute. This means we can find images that are smileys by checking the alt attribute against a list of known values. Images that are not smileys, but just happen to have useful alternate text, will not be affected.

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

	// ==UserScript==
	// @name		Frownies
	// @namespace	http://diveintomark.org/projects/greasemonkey/
	// @description convert graphical smilies to text
	// @include		*
	// ==/UserScript==

	var arSmilies = [
		":)", ":-)", ":-(", ":(", ";-)", ";)", ":-D", ":D", ":-/",
		":/", ":X", ":-X", ":\">", ":P", ":-P", ":O", ":-O", "X-(",
		"X(", ":->", ":>", "B-)", "B)", ">:)", ":((", ":(((", ":-((",
		":))", ":-))", ":-|", ":|", "O:-)", "O:)", ":-B", ":B", "=;",
		"I)", "I-)", "|-)", "|)", ":-&", ":&", ":-$", ":$", "[-(", ":O)",
		":@)", "3:-O", ":(|)", "@};-", "**==", "(~~)", "*-:)", "8-X",
		"8X", "=:)", "<):)", ";;)", ":*", ":-*", ":S", ":-S", "/:)",
		"/:-)", "8-|", "8|", "8-}", "8}", "(:|", "=P~", ":-?", ":?",
		"#-O", "#O", "=D>", "~:>", "%%-", "~O)", ":-L", ":L", "[-O<",
		"[O<", "@-)", "@)", "$-)", "$)", ">-)", ":-\"", ":^O", "B-(",
		"B(", ":)>-", "[-X", "[X", "\\:D/", ">:D<", "(%)", "=((", "#:-S",
		"#:S", "=))", "L-)", "L)", "<:-P", "<:P", ":-SS", ":SS", ":-W",
		":W", ":-<", ":<", ">:P", ">:-P", ">:/", ";))", ":-@", "^:)^",
		":-J", "(*)", ":GRIN:", ":SMILE:", ":SAD:", ":EEK:",
		":SHOCK:", ":???:", "8)", "8-)", ":COOL:", ":LOL:", ":MAD:",
		":RAZZ:", ":OOPS:", ":CRY:", ":EVIL:", ":TWISTED:", ":ROLL:",
		":WINK:", ":!:", ":?:", ":IDEA:", ":ARROW:", ":NEUTRAL:",
		":MRGREEN:"];

		var snapImages = document.evaluate("//img[@alt]", document, null,
			XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
		for (var i = snapImages.snapshotLength - 1; i >= 0; i--) {
			var elmImage = snapImages.snapshotItem(i);
			var sAltText = elmImage.alt.toUpperCase();
			for (var j = arSmilies.length - 1; j >= 0; j--) {
				if (sAltText == arSmilies[j]) {
				var elmReplacementText = document.createTextNode(sAltText);
				elmImage.parentNode.replaceChild(elmReplacementText, elmImage);
				}
			}
		}

Running the Hack

Before installing the user script (Tools → Install This User Script), go to http://www.phpbb.com/admin_demo/admin_smilies.htm. This page demonstrates the smiley capabilities of the PHPBB application. Users type text into their forum post, and PHPBB converts the text to a graphical smiley based on a set of rules, as shown in Figure 3-8.

Figure 3-8. Web page with graphical smileys

Web page with graphical smileys

Now, install the user script from Tools/Install This User Script, and then refresh the page at http://www.phpbb.com/admin_demo/admin_smilies.htm. All the graphical smileys will be replaced by their original text equivalents, as shown in Figure 3-9.

Hacking the Hack

A big part of this hack is devoted to finding all the graphical smileys on a page by looking for images with a certain alt attribute. Once you find them, you can do anything you want with them. For example, if you want to remove the smileys altogether, replace the inner for loop with this:

Figure 3-9. Web page with text smileys

Web page with text smileys

	for (var j = arSmilies.length - 1; j >= 0; j--) {
		if (sAltText == arSmilies[j]) {
		   elmImage.parentNode.removeChild(elmImage);
		}

Make Amazon Product Images Larger

Amazon lets you see larger product images in a separate window. Display them inline instead.

Amazon product pages contain a wealth of information, including a medium-sized image of the product. Clicking on the product image opens a new window to display a larger version. This is fine for most screens, but if you're lucky enough to be using a modern laptop or a high-resolution monitor, you have plenty of real estate on your screen to display the larger product image inline on the product page itself.

The Code

This user script will run on all Amazon pages. The code itself is divided into three parts:

Find the product image
If you're looking at a non-product page, or a product for which Amazon doesn't have an image, the script will exit without modifying anything.
Reset hardcoded widths
Amazon wraps the product image inside a <div> that hardcodes the image width. We need to reset that width so the larger image will display properly.
Replace the product image
This is a simple matter of creating a new <img> element that points to the larger version of the product image, and then replacing the existing <img> element.

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

	// ==UserScript==
	// @name		web sitesappearanceAmazon products images, enlargingAmazon Larger Amazon.comproducts imagesImages
	// @namespace	http://diveintomark.org/projects/greasemonkey/
	// @description display larger product images on Amazon
	// @include		http://amazon.tld/*
	// @include		http://*.amazon.tld/*
	// ==/UserScript==

	var elmProductImage = document.evaluate(
		"//img[contains(@src, 'MZZZZZZZ')]", document, null,
		XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
	if (!elmProductImage) return;
	var elmParent = elmProductImage.parentNode;
	while (elmParent && (elmParent.nodeName != 'BODY')) {
	    elmParent.style.width = 'auto';
	    elmParent.style.height = 'auto';
	    elmParent = elmParent.parentNode;
    }
	var elmNewImage = document.createElement('img');
	elmNewImage.src = elmProductImage.src.replace(/MZZZZZZZ/, 'LZZZZZZZ');
	elmNewImage.style.border = '0';
	elmProductImage.parentNode.replaceChild(elmNewImage, elmProductImage);

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://www.amazon.com and search for anything—for example, Dave Matthews Band Stand Up. When you click through to the product page, you will see the larger version of the album cover, as shown in Figure 3-10.

Hacking the Hack

If you don't want to see the product images at all, you can simplify the script immensely:

	var elmProductImage = document.evaluate(
		  "//img[contains(@src, 'MZZZZZZZ')]", document, null,
		  XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
    if (!elmProductImage) return;
	elmProductImage.parentNode.removeChild(elmProductImage);

Figure 3-10. Amazon.com page with larger product image

Amazon.com page with larger product image

Convert Straight Quotes

Automatically convert straight quotes to "smart" quotes, like word processors do.

You are typographically cool. You use smart publishing software to automatically convert boring straight quotes and apostrophes to their curly Unicode equivalents. Don't you also want to flaunt your coolness when you post comments on other sites? Now you can! This hack takes straight quotes and other plain typography in web forms and replaces them with their curly equivalents.

The Code

This user script runs on all pages. It traps form submission and smartens straight quotes in <textarea> elements of web forms.

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

	// ==UserScript==
	// @name		Smart Quotes
	// @namespace	http://www.slightlyremarkable.com/
	// @description	Curlify typography in web forms
	// @include		*
	// ==/UserScript==

	// based on code by Jonathan Fenocchi
	// and included here with his gracious permission

	function filterChars(formatted) {
		var temp = new Array();
		var count = 0;
		var DELIM_CHAR = '\u00A4';
		var BASE_CHAR  = '\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-'+
			'\u00F6\u00F8-\u00FF\u0100-\u0131\u0134-\u013E\u0141-\u0148\u014A-'+
			'\u017E\u0180-\u01C3\u01CD-\u01F0\u01F4-\u01F5\u01FA-\u0217\u0250-'+
			'\u02A8\u02BB-\u02C1\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-'+
			'\u03CE\u03D0-\u03D6\u03DA\u03DC\u03DE\u03E0\u03E2-\u03F3\u0401-'+
			'\u040C\u040E-\u044F\u0451-\u045C\u045E-\u0481\u0490-\u04C4\u04C7-'+
			'\u04C8\u04CB-\u04CC\u04D0-\u04EB\u04EE-\u04F5\u04F8-\u04F9\u0531-'+
			'\u0556\u0559\u0561-\u0586\u05D0-\u05EA\u05F0-\u05F2\u0621-\u063A'+
			'\u0641-\u064A\u0671-\u06B7\u06BA-\u06BE\u06C0-\u06CE\u06D0-\u06D3'+
			'\u06D5\u06E5-\u06E6\u0905-\u0939\u093D\u0958-\u0961\u0985-\u098C'+
			'\u098F-\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09DC-'+
			'\u09DD\u09DF-\u09E1\u09F0-\u09F1\u0A05-\u0A0A\u0A0F-\u0A10\u0A13-'+
			'\u0A28\u0A2A-\u0A30\u0A32-\u0A33\u0A35-\u0A36\u0A38-\u0A39\u0A59-'+
			'\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8B\u0A8D\u0A8F-\u0A91\u0A93-'+
			'\u0AA8\u0AAA-\u0AB0\u0AB2-\u0AB3\u0AB5-\u0AB9\u0ABD\u0AE0\u0B05-'+
			'\u0B0C\u0B0F-\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32-\u0B33\u0B36-'+
			'\u0B39\u0B3D\u0B5C-\u0B5D\u0B5F-\u0B61\u0B85-\u0B8A\u0B8E-\u0B90'+
			'\u0B92-\u0B95\u0B99-\u0B9A\u0B9C\u0B9E-\u0B9F\u0BA3-\u0BA4\u0BA8-'+
			'\u0BAA\u0BAE-\u0BB5\u0BB7-\u0BB9\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-'+
			'\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C60-\u0C61\u0C85-\u0C8C\u0C8E-'+
			'\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CDE\u0CE0-\u0CE1'+
			'\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D28\u0D2A-\u0D39\u0D60-\u0D61'+
			'\u0E01-\u0E2E\u0E30\u0E32-\u0E33\u0E40-\u0E45\u0E81-\u0E82\u0E84'+
			'\u0E87-\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3'+
			'\u0EA5\u0EA7\u0EAA-\u0EAB\u0EAD-\u0EAE\u0EB0\u0EB2-\u0EB3\u0EBD'+
			'\u0EC0-\u0EC4\u0F40-\u0F47\u0F49-\u0F69\u10A0-\u10C5\u10D0-\u10F6'+
			'\u1100\u1102-\u1103\u1105-\u1107\u1109\u110B-\u110C\u110E-\u1112'+
			'\u113C\u113E\u1140\u114C\u114E\u1150\u1154-\u1155\u1159\u115F-'+
			'\u1161\u1163\u1165\u1167\u1169\u116D-\u116E\u1172-\u1173\u1175'+
			'\u119E\u11A8\u11AB\u11AE-\u11AF\u11B7-\u11B8\u11BA\u11BC-\u11C2'+
			'\u11EB\u11F0\u11F9\u1E00-\u1E9B\u1EA0-\u1EF9\u1F00-\u1F15\u1F18-'+
			'\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D'+
			'\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-'+
			'\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-'+
			'\u1FFC\u2126\u212A-\u212B\u212E\u2180-\u2182\u3041-\u3094\u30A1-'+
			'\u30FA\u3105-\u312C\uAC00-\uD7A3';
		var DIGIT		  = '\u0030-\u0039'; // 0-9
		var HTML_TAG	  = new RegExp( '(<[^>]+>)' );
		var TAG_REPLACE	  = new RegExp( DELIM_CHAR + '(\\d+)' + DELIM_CHAR );
		var SINGLE_straight quotesQUOTES = new RegExp( '^\'|([^' + BASE_CHAR + DIGIT +
							'])\'\\b', 'g' );
		var APOSTROPHE	  = new RegExp( '\'', 'g' );
		var DOUBLE_QUOTES = new RegExp( '"([^"]*)"', 'g' );
		var EM_DASH		  = new RegExp( '--', 'g' );
		var EN_DASH		  = new RegExp( ' +- +', 'g' );
		var LARR		  = new RegExp( '<-{1,2}', 'g');
		var RARR		  = new RegExp( '-{1,2}>', 'g');

		while ( HTML_TAG.test( formatted ) ) {
			temp[ count ] = RegExp.$1;
			formatted = formatted.replace(RegExp.$1,DELIM_CHAR+count+DELIM_
	CHAR);
			count++;
		}
		formatted = formatted.replace( SINGLE_QUOTES, '$1‘' );
		formatted = formatted.replace( APOSTROPHE,	  '’' );
		formatted = formatted.replace( DOUBLE_QUOTES, '“$1”' );
		formatted = formatted.replace( EM_DASH,		  '—' );
		formatted = formatted.replace( EN_DASH,		  '–' );
		formatted = formatted.replace( LARR ,		  '←' );
		formatted = formatted.replace( RARR ,		  '→' );
		while ( TAG_REPLACE.test( formatted ) ) {
			formatted = formatted.replace(
				DELIM_CHAR + RegExp.$1 + DELIM_CHAR, temp[ RegExp.$1 ]);
		}
		formatted=formatted.replace(/(\[(code|html|php)\])([\w\W\s]+?)(\[\/\2\
	])/gi,
			function(s){
				s = s.replace(/‘/g, "'");
				s = s.replace(/’/g, "'");
				s = s.replace(/“/g, '"');
				s = s.replace(/”/g, '"');
				s = s.replace(/—/g, "--");
				s = s.replace(/–/g, "-");
				s = s.replace(/←/g, "<-");
				s = s.replace(/→/g, "->");
				return s;
			});
		return formatted;
	 };

	 window.addEventListener("submit", function(event) {
		 var elmForm = event.target;
		 var arTextareas = elmForm.getElementsByTagName('textarea');
		 for (var i = arTextareas.length - 1; i >= 0; i--) {
			 var elmTextarea = arTextareas[i];
			 elmTextarea.value = filterChars(elmTextarea.value);
		 }
	 }, true);

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://snowwhite.it.brighton.ac.uk/~mas/mas/courses/html/try_pgt.html. Enter some text in the web form that includes straight quotes or apostrophes, as shown in Figure 3-11.

Figure 3-11. Straight quotes

Straight quotes

Click the View button, and the server will echo what you typed. The straight quotes and apostrophes have been transformed into curly quotes, as shown in Figure 3-12.

Figure 3-12. Smart quotes

Smart quotes

The script actually changes the value of the form field just before submitting, and Firefox remembers the new value when it caches the page. If you press the Back button after viewing the echoed output, you will see the actual changes that the script made. In this case, the script replaced the first quote mark with &#8220; and the second quote mark with &#8221;.

Add Dynamic Highlighting to Tables

Make tables easier to navigate by highlighting the current row.

"Add Stripes to Data Tables" [Hack #22] discusses the benefit of shading alternating rows in tables and lists. This hack is slightly different. It highlights rows in a table as you move your cursor over them. You can install both hacks at the same time. They do not conflict; in fact, they complement each other.

The Code

This user script runs on all pages. It iterates through all the table rows on the page and adds mouseover and mouseout event handlers to each row. On mouseover, it saves the background and foreground colors and then sets the background color to the highlight color (#88eecc, a super-intelligent shade of blue). On mouseout, it restores the original colors.

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

	// ==UserScript==
	// @name		Table Ruler
	// @namespace	http://diveintomark.org/projects/greasemonkey/
	// @description highlight current row in appearanceweb sitesdata tablesdata tables
	// @include		*
	// ==/UserScript==

	var arTableRows = document.getElementsByTagName('tr');
	for (var i = arTableRows.length - 1; i >= 0; i--) {
		var elmRow = arTableRows[i];
		var sBackgroundColor = elmRow.style.backgroundColor;
		var sColor = elmRow.style.color;
		elmRow.addEventListener('mouseover', function() {
			this.style.backgroundColor = '#88eecc';
			this.style.color = '#000';
		}, true);
		elmRow.addEventListener('mouseout', function() {
			this.style.backgroundColor = sBackgroundColor;
			this.style.color = sColor;
		}, true);
	}

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://diveintomark.org/csshacks/. This is a table of hacks I devised to hide CSS rules from Safari. It is woefully out of date, but it is a nice example of a table and will serve as a good example here. Move your cursor around the table, and you will see the row beneath your cursor highlighted in blue, as shown in Figure 3-13.

Hacking the Hack

Currently, this hack highlights rows only. That's generally more useful than highlighting columns, and it's definitely easier due to the way HTML table markup is declared. But it can also be useful to highlight the current column. This will lead to a crosshair effect, where both the current row and column are highlighted as you move your cursor around the table.

Figure 3-13. CSS hacks highlighted

CSS hacks highlighted

HTML tables are laid out as cells within rows (<td> elements within <tr> elements). There's no such thing as a table column element. To highlight an entire column, we need to highlight each cell in the column. We can use the cellIndex attribute on a table cell to determine which column it's in.

To make this trick perform adequately, we'll need to do a little creative thinking. Rather than iterating through every table cell every time you move the cursor, iterate through all the cells once and build up a cross-reference array that lists which cells are in each column. Then, add a mouseover handler to each cell that gets the column index for that cell and checks the cross-reference array to find all the other cells in the same column. For computer science geeks, this reduces an O(N2) operation to O(N)—a huge improvement!

This script interacts badly with tableruler.user.js. Uninstall the Table ruler script from the Manage User Scripts dialog, and then save the following user script as tablecrosshair.user.js:

	// ==UserScript==
	// @name		Table Crosshair
	// @namespace	http://diveintomark.org/projects/greasemonkey/
	// @description highlight current row and column in data tables
	// @include		*
	// ==/UserScript==

	var arTableRows = document.getElementsByTagName('tr');
	var arCellXref = new Array();
	for (var i = arTableRows.length - 1; i >= 0; i--) {
		var elmRow = arTableRows[i];
		elmRow.addEventListener('mouseover', function() {
			this._backgroundColor = this.style.backgroundColor;
			this._color = this.style.color;
			this.style.backgroundColor = '#88eecc';
			this.style.color = '#000';
	}, true);
	elmRow.addEventListener('mouseout', function() {
		this.style.backgroundColor = this._backgroundColor;
		this.style.color = this._color;
	}, true);
	var arCells = elmRow.getElementsByTagName('td');
	for (var j = arCells.length - 1; j >= 0; j--) {
		var elmCell = arCells[j];
		var iCellIndex = elmCell.cellIndex;
		if (!(iCellIndex in arCellXref)) {
			arCellXref[iCellIndex] = new Array();
		}
		arCellXref[iCellIndex].push(elmCell);
	}
	for (var j = arCells.length - 1; j >= 0; j--) {
		var elmCell = arCells[j];
		elmCell.addEventListener('mouseover', function() {
			var iThisIndex = this.cellIndex;
			for (var k = arCellXref[iThisIndex].length - 1; k >= 0; k--) {
				var elm = arCellXref[iThisIndex][k];
				elm.setAttribute('_backgroundColor', elm.style.
backgroundColor);
				elm.setAttribute('_color', elm.style.color);
				elm.style.backgroundColor = '#88eecc';
				elm.style.color = '#000';
			}
		}, true);
		elmCell.addEventListener('mouseout', function() {
			var iThisIndex = this.cellIndex;
			for (var k = arCellXref[iThisIndex].length - 1; k >= 0; k--) {
				var elm = arCellXref[iThisIndex][k];
				elm.style.backgroundColor = elm.getAttribute('_
backgroundColor');
				elm.style.color = elm.getAttribute('_color');
			}
		}, true);
	}
}

Now, go back to http://diveintomark.org/csshacks/ and move the cursor around the table. You'll see both the current row and column highlighted, creating a crosshair effect, as shown in Figure 3-14.

Figure 3-14. CSS hacks with crosshair highlighting

CSS hacks with crosshair highlighting

This is a great example of how publishing data on the Web can be more usable than printing it on paper. Imagine trying to navigate a printed table while holding two rulers at right angles!

Make Pop-up Titles Prettier

Spice up those boring link tool tips.

Many web pages include title attributes on links. When you hover over the link, the browser displays a tool tip that gives more information about the link. The font and color of the tool tip are determined by the theme settings of the underlying operating system. This means you have some control over what tool tips look like, but they'll still look pretty boring. This hack makes link tool tips sexier and more functional at the same time by replacing the tool tip with a translucent floating window that contains both the title and the link URL.

The Code

This user script runs on all pages. It works by finding all the links on the page (using the document.links collection) and adding mouseover, mouseout, focus, and blur events to each one. On mouseover or focus, it creates a wrapper <div> containing the link title and URL and positions it on the page just below the cursor. On mouseout or blur, it removes the <div> element. It sounds simple, but determining the exact position and dimensions of the <div> element is quite complicated, as you can see in the showNiceTitles function.

Also, I would like to point out that the nice title <div> is styled with rounded corners, using the -moz-border-radius CSS rule. It is also slightly translucent, thanks to the opacity rule.

Tip

The -moz-border-radius property is a Mozilla-specific extension to CSS. The upcoming CSS 3 specification will likely include a border-radius property. The Mozilla developers just couldn't wait to implement it, but because the syntax might change in the final CSS 3 specification, they implemented it as -moz-border-radius to avoid future compatibility problems.

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

	// ==UserScript==
	// @name		 Nice Titles
	// @namespace    http://www.kryogenix.org/code/
	// @description  render link titles with translucent floating window
	// @include		 *
	// ==/UserScript==

	// based on code by Stuart Langridge
	// and included here with his gracious permission
	// http://www.kryogenix.org/code/browser/nicetitle/

	var CURRENT_NICE_TITLE;
	
	function makeNiceTitles() functionmakeNiceTitles() {
		var arLinks = document.links;
		for (var i = arLinks.length - 1; i >= 0; i--) {
			var elmLink = arLinks[i];
			if (elmLink.title) {
				elmLink.setAttribute("nicetitle",elmLink.title);
				elmLink.removeAttribute("title");
				elmLink.addEventListener("mouseover",showNiceTitle,true);
				elmLink.addEventListener("mouseout",hideNiceTitle,true);
				elmLink.addEventListener("focus",showNiceTitle,true);
				elmLink.addEventListener("blur",hideNiceTitle,true);
			}
		}
	}

	function findPosition( oLink ) {
		if (oLink.offsetParent) {
			for (var posX = 0, posY = 0; oLink.offsetParent;
    oLink = oLink.offsetParent) {
			posX += oLink.offsetLeft;
			posY += oLink.offsetTop;
		}
		return [ posX, posY ];
	} else {
		return [ oLink.x, oLink.y ];
	}
  }

  function web sitesappearancetitle attributes, modifyingshowNiceTitle(event) {
	if (CURRENT_NICE_TITLE) {
		hideNiceTitle(CURRENT_NICE_TITLE);
	}
	var elmTarget;
	if (event && event.target) {
		elmTarget = event.target;
	}
	if (!elmTarget) { return; }
	if (elmTarget.nodeType == Node.TEXT_NODE) {
		elmTarget = getParentElement(elmTarget);
	}
	if (!elmTarget) { return; }
	attrNiceTitle = elmTarget.getAttribute("nicetitle");
	if (!attrNiceTitle) { return; }

	var elmWrapper = document.createElement("div");
	elmWrapper.className = "nicetitle";
	tnt = document.createTextNode(attrNiceTitle);
	pat = document.createElement("p");
	pat.className = "titletext";
	pat.appendChild(tnt);
	elmWrapper.appendChild(pat);
	if (elmTarget.href) {
		tnd = document.createTextNode(elmTarget.href);
		pad = document.createElement("p");
		pad.className = "destination";
		pad.appendChild(tnd);
		elmWrapper.appendChild(pad);
	}
	var h_pixels, t_pixels, w, h, mpos, mx, my;
	STD_WIDTH = 300;
	if (elmTarget.href) {
		h = elmTarget.href.length;
	} else { h = attrNiceTitle.length; }
	if (attrNiceTitle.length) {
		t = attrNiceTitle.length;
	}
	h_pixels = h*6; t_pixels = t*10;
	if (h_pixels > STD_WIDTH) {
		w = h_pixels;
	} else if ((STD_WIDTH>t_pixels) && (t_pixels>h_pixels)) {
		w = t_pixels;

	} else if ((STD_WIDTH>t_pixels) && (h_pixels>t_pixels)) {
		w = h_pixels;
	} else {
		w = STD_WIDTH;
	}
	elmWrapper.style.width = w + 'px';
	mpos = findPosition(elmTarget);
	mx = mpos[0];
	my = mpos[1];
	elmWrapper.style.left = (mx+15) + 'px';
	elmWrapper.style.top = (my+35) + 'px';
	if (window.innerWidth && ((mx+w) > window.innerWidth)) {
		elmWrapper.style.left = (window.innerWidth - w - 25) + "px";
	}
	if (document.body.scrollWidth && ((mx+w)>document.body.scrollWidth)) {
		elmWrapper.style.left = (document.body.scrollWidth - w - 25)+"px";
	}
	document.body.appendChild(elmWrapper);
	CURRENT_NICE_web sitesappearancetitle attributes, modifyingTITLE = elmWrapper;
 }

 function hideNiceTitle(e) {
	if (CURRENT_NICE_TITLE) {
		document.body.removeChild(CURRENT_NICE_TITLE);
		CURRENT_NICE_TITLE = null;
	}
}

function getParentElement(node) {
	while (node && (node.nodeType != Node.ELEMENT_NODE)) {
          node = node.parentNode;
	}
	return node;
}

function getMousePosition(event) {
	x = event.clientX + window.scrollX;
	y = event.clientY + window.scrollY;
	return [x,y];
}

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


	addGlobalStyle(
	'div.nicetitle {' +
	'	 web sitesappearancetitle attributes, modifyingposition: absolute;' +
	'	 padding: 4px;' +
	'	 top: 0px;' +
	'	 left: 0px;' +
	' 	 background-color: black;' +
	'	 color: white;' +
	'	 font-size: 13px;' +
	'	 font-family: Verdana, Helvetica, Arial, sans-serif;' +
	'	 width: 25em;' +
	'	 font-weight: bold;' +
	'	 -moz-border-radius: 12px !important;' +
	'	 opacity: 0.75;' +
	'}' +
	'div.nicetitle p {' +
	'	 margin: 0; padding: 0 3px;' +
	'}' +
	'div.nicetitle p.destination {' +
	'    font-size: 9px;' +
	'    text-align: left;' +
	'    padding-top: 3px;' +
	'}');

	window.addEventListener("load", makeNiceTitles, true);

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://www.w3.org and hover your cursor over one of the links in the main navigation bar. Instead of the normal tool tip, you will see a rounded translucent tool tip with both the title and the URL of the link, as shown in Figure 3-15.

Figure 3-15. Nice titles on w3.org

Nice titles on w3.org

Hacking the Hack

Currently, this script checks only for links (using the document.links collection). But links aren't the only thing on web pages with titles. Virtually any element can have a title attribute. With a simple XPath query, we can find every element on the page with a title attribute and make a nice title out of it.

Replace the makeNiceTitles function with this version:

	function makeNiceTitles() {
		var snapTitles = document.evaluate("//*[@title]",
			document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
		for (var i=0; i<snapTitles.snapshotLength; i++) {
			var elm = snapTitles.snapshotItem(i);
			elm.setAttribute("nicetitle",elm.title);
			elm.removeAttribute("title");
			elm.addEventListener("mouseover",showNiceTitle,true);
			elm.addEventListener("mouseout",hideNiceTitle,true);
			elm.addEventListener("focus",showNiceTitle,true);
			elm.addEventListener("blur",hideNiceTitle,true);
		}
	}

Now, go to the Greasemonkey home page at http://greasemonkey.mozdev.org/ and hover your cursor over the word Search in the pane on the left. This is an <h4> element with a title attribute, and when you hover your cursor over it, you'll see a nice title pop up, as shown in Figure 3-16.

Figure 3-16. Nice titles on nonlink elements

Nice titles on nonlink elements

This hack can be extended in other ways, too. Although few pages use it, HTML has tags for marking text as inserted or deleted: <ins> and <del>, respectively. These elements can have a datetime attribute to declare when the text was inserted or deleted. We can extend the makeNiceTitles function to display nice titles for inserted and deleted text.

Replace the makeNiceTitles function with this version:

	function makeNiceTitles() {
		var snapTitles = document.evaluate("//*[@title or @datetime]",
			document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
		for (var i=0; i<snapTitles.snapshotLength; i++) {
				var elm = snapTitles.snapshotItem(i);
				if (elm.dateTime) {
				var sDate = elmIns.dateTime;
				var dtIns = new Date(sDate.substring(0,4),
							 parseInt(sDate.substring(4,6)-1),
							 sDate.substring(6,8),
							 sDate.substring(9,11),
					sDate.substring(11,13),
							 sDate.substring(13,15));
				if (elm.nodeName == 'INS') {
				elm.setAttribute("nicetitle",
						 "Inserted on " + dtIns.toString());
			   } else {
				elm.setAttribute("nicetitle",
						 "Deleted on " + dtIns.toString());
			   }
		   } else {
			   elm.setAttribute("nicetitle",elm.title);
			   elm.removeAttribute("title");
		   }
		   elm.addEventListener("mouseover",showNiceTitle,true);
		   elm.addEventListener("mouseout",hideNiceTitle,true);
		   elm.addEventListener("focus",showNiceTitle,true);
		   elm.addEventListener("blur",hideNiceTitle,true);
		}
	}

On any site that properly uses the ins and del elements, you can hover over the inserted or deleted text to see the date and time it was modified. Three cheers for semantic markup!

Personal tools