Greasemonkey Hacks/Those Not Included in This Classification

From WikiContent

< Greasemonkey Hacks
Revision as of 20:29, 11 March 2008 by Docbook2Wiki (Talk)
(diff) ←Older revision | Current revision (diff) | Newer revision→ (diff)
Jump to: navigation, search
Greasemonkey Hacks


Contents

Hacks 95–100: Introduction

Even the most well-planned categorization scheme needs one last bucket called Other. As Jorge Luis Borges famously wrote:

These ambiguities, redundancies, and deficiencies recall those attributed by Dr. Franz Kuhn to a certain Chinese encyclopedia entitled Celestial Emporium of Benevolent Knowledge. On those remote pages it is written that animals are divided into (a) those that belong to the Emperor, (b) embalmed ones, (c) those that are trained, (d) suckling pigs, (e) mermaids, (f) fabulous ones, (g) stray dogs, (h) those that are included in this classification, (i) those that tremble as if they were mad, (j) innumerable ones, (k) those drawn with a very fine camel's hair brush, (l) others, (m) those that have just broken a flower vase, (n) those that resemble flies from a distance.

These are hacks that didn't fit anywhere else. That is not to say that they are trivial or unimpressive. Trust me, I've saved the best for last.

Maximize HomestarRunner Cartoons

Make Flash animations fill the entire browser window.

One of my guilty pleasures on the Web is HomestarRunner.com (http://www.homestarrunner.com). I say "guilty pleasure" for two reasons: first, because it serves no purpose whatsoever except entertainment, and second, because it's entirely Flash-based, and I normally avoid Flash if at all possible. But HomestarRunner is just too good to stay away from. It's why I keep Flash installed at all.

Here's the problem: I run my laptop at 1400 x 1050, and the HomestarRunner cartoons look downright puny, because they display at a fixed size. This hack intelligently resizes the HomestarRunner cartoons to fill my browser window. The cartoons still look good because they are drawn with vector graphics, so Flash scales them without introducing blotches or jagged edges.

The Code

This user script runs only on http://www.homestarrunner.com. It finds the two Flash objects on the page; the first is the cartoon itself, and the second is the site navigation bar. It determines the optimal dimensions to fill the browser window without exceeding the height or width and resizes the objects to fit.

Save the following user script as homestar-fullon.user.js:

	// ==UserScript==
	// @name			Homestar-Fullon
	// @namespace		http://apps.bcheck.net/greasemonkey/
	// @description		Make HomestarRunner cartoonsHomeStarRunner cartoons fill your browser window
	// @include			http://homestarrunner.com/*
	// @include			http://www.homestarrunner.com/*
	// ==/UserScript==

	// based on code by Timothy Rice
	// and included here with his gracious permission

	function resize() {
		var objs = document.getElementsByTagName('embed');
		var o = objs[0];
		var bar = objs[1];

		if(o && o.width && o.height && o.width>0 && o.height>0) {
			var dw = window.innerWidth;
			var dh = window.innerHeight - (bar&&bar.height?bar.height*2:0);
			var ar = o.width/o.height;
			if (dw/ar <= dh) {
				dh = Math.floor(dw / ar);
			} else {
				dw = Math.floor(dh * ar);
			}

			/* set embedded object's size */
			o.width = dw;
			o.height = dh;
		}
	}

	/* remove margin */
	document.body.style.margin = "0px";

	/* resize embed when window is resized */
	window.addEventListener("resize", resize, false);

	/* resize on first load */
	resize();

Running the Hack

Before you install this script, maximize your browser window and go to http://www.homestarrunner.com. Regardless of your monitor's resolution, the cartoon will always be the same size, centered in the window with tons of blank space on either side, as shown in Figure 12-1.

Figure 12-1. HomestarRunner.com, fixed size

HomestarRunner.com, fixed size

Now, install the user script (Tools Install This User Script) and refresh the page. Bam! The cartoon resizes to fill as much of your browser window as → possible, as shown in Figure 12-2.

Figure 12-2. HomestarRunner.com, maximized

HomestarRunner.com, maximized

Depending on the dimensions of your browser window, the cartoon might fill the height of the window with space on the left and right, or it might fill the width and leave space on the top and bottom. The script is smart enough to figure out the maximum dimensions of the Flash animation. It even resizes the animation as you resize your browser window.

Refine Your Google Search

Google might already know what keywords you should add to your search to find exactly what you're looking for.

As described in "Autocomplete Search Terms as You Type" [Hack #55], you can visit http://www.google.com/webhp?complete=1 and start typing, and Google Suggest will autocomplete your query as you type. By itself, this is wickedly cool. Now, let's make it even cooler by integrating it into the main Google web search. Along with the usual search results, you'll see a list of related queries made up of additional keywords, so you can refine your search.

The Code

Google Suggest works by requesting a specially constructed URL with the characters you've typed so far. The request returns JavaScript code, and Google Suggest evaluates this code and adds the results to its autocomplete menu. If you type a complete keyword, followed by a space, Google Suggest will return a list of popular searches that include your keyword plus one or two other words.

For example, if you type firefox, Google Suggest constructs this URL:

	http://www.google.com/complete/search?js=true&qu=firefox 

Enter that URL in your location bar and you'll see Google's response:

	sendRPCDone(frameElement, "firefox", new Array("firefox", "firefox
	download",
	"firefox browser", "firefox extensions", "firefox plugins", "firefox
	mozilla",
	"firefox themes", "firefox.com", "firefox web browser", "firefox 1.0"),
	new Array("25,900,000 results", "8,000,000 results", "6,990,000 results",
	"1,270,000 results", "1,250,000 results", "8,160,000 results",
	"1,950,000 results", "1 result", "5,460,000 results", "6,540,000 results"),
	new Array(""));
	[end example]

In other words, Google is already doing the hard part: tracking billions of queries and ranking them by popularity. Compared to that, constructing the request and parsing the response is easy. You can mimic Google's autocomplete algorithm by constructing the URL yourself, calling GM_ xmlhttpRequest, and parsing the response.

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

	// ==UserScript==
	// @name			Refine Your Search
	// @namespace		http://diveintomark.org/projects/greasemonkey/
	// @description		adds a "refine your search" list on searchingGoogle searchesGoogle search results
	// @include			http://www.google.tld/search*
	// ==/UserScript==
	
	function getCurrentSearchText() {
		var elmForm = document.forms.namedItem('gs');
		if (!elmForm) { return; }
		var elmSearchBox = elmForm.elements.namedItem('q');
		if (!elmSearchBox) { return; }
		var usQuery = elmSearchBox.value;
		if (!usQuery) { return; }
		return usQuery;
	}

	function getFirstSearchResult() {
		var results = document.evaluate("//p[@class='g']", document, null,
			XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
		return results.snapshotLength ? results.snapshotItem(0) : null;
	}

	function parseRefineYourSearchResults(oResponse) {
		if (oResponse.responseText.indexOf('new Array(') == -1) return;
		var arResults = oResponse.responseText.split(
			'new Array("')[1].split('")')[0].split('", "');
		var usQuery = getCurrentSearchText();
		var htmlArResults = new Array();
		for (var i = 0; i < arResults.length; i++) {
			if (!arResults[i] || (arResults[i] == usQuery)) continue;
			htmlArResults.push('<a href="http://www.google.com/search?q=' +
					escape(arResults[i]) + '">' +
					arResults[i] + '</a>');
		}
		if (!htmlArResults.length) return;
		var elmRefine = document.createElement('div');
		elmRefine.id = 'refineyoursearch';
		elmRefine.style.fontSize = 'small';
		elmRefine.style.paddingTop = elmRefine.style.paddingBottom = '1em';
		var html = 'Refine your search: ' + htmlArResults.join(' &middot; ');
		elmRefine.innerHTML = html;	
		var elmFirstResult = getFirstSearchResult();
		elmFirstResult.parentNode.insertBefore(elmRefine, elmFirstResult);
	}
	
	var usQuery = getCurrentSearchText();
	if (!usQuery) return;
	if (!getFirstSearchResult()) return;
	GM_xmlhttpRequest({
		method: "GET",
searchingGoogle searches
		url: "http://www.google.com/complete/search?hl=en&js=true&qu=" +
			  escape(usQuery + ' '),
		onload: parseRefineYourSearchResults
	});

Running the Hack

After installing the user script from Tools → Install This User Script, go to http://www.google.com and search for firefox. Before the first search result, you'll see a list of related queries, as shown in Figure 12-3.

Figure 12-3. Google search for "firefox" with suggested refined searches

Google search for "firefox" with suggested refined searches

If you click on one of the suggested refined searches, such as firefox plugins, Google displays those search results, which include suggestions for even further refinements, as shown in Figure 12-4. Depending on your keywords, you might be able to drill down several levels, until Google finally runs out of suggestions.

Figure 12-4. Google search for "firefox plugins" with suggestions

Google search for "firefox plugins" with suggestions

Google Suggest works only on web searches, and only in English, so this hack inherits those limitations. You can read more about Google Suggest in Google's FAQ (http://labs.google.com/suggestfaq.html).

Check Whether Pages Really Validate

If someone puts a badge on her site claiming to be "Valid XHTML," run it through the W3C Validator and see if she's telling the truth.

You've probably seen them on personal weblogs or wikis. Stuffed down at the bottom of the page, amidst the copyright notices and privacy policies, a cluster of badges proudly proclaims, "This site is valid XHTML!" "This site is valid CSS!" "This site validates against some arcane standard you've never heard of!" But do they really validate? Let's find out.

The Code

This user script runs on all pages. It takes advantage of a feature of the W3C HTML Validator, which suggests that people put a link on their sites that points to http://validator.w3.org/check/referer. That URL uses the HTTP Referer header to automatically check the page you came from. If the page uses valid markup, the validator service will proclaim "This Page Is Valid (X)HTML"; otherwise, it will list all the errors on the page.

If the script finds such a link, it uses Greasemonkey's GM_xmlhttpRequest function to call the W3C validation service in the background and get the results. If it turns out that the page is not really valid, we replace the original link with a new link that reads "Invalid markup!"

Save the following script as reallyvalid.user.js:

	// ==UserScript==
	// @name			Really Valid?
	// @namespace		http://diveintomark.org/projects/greasemonkey/
	// @description		check if pages claiming to be valid (X)HTML really are
	// @include *
	// ==/UserScript==

	var snapValidLinks = document.evaluate(
		"//a[@href='http://validator.w3.org/check/referer']",
		document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
	if (!snapValidLinks.snapshotLength) return;
	GM_xmlhttpRequest({
		method: 'GET',
		url: 'http://validator.w3.org/check?uri=' + escape(location),
		onload: function(oResponse) {
			if (/This Page Is Valid/.test(oResponse.responseText)) return;
			for (var i = 0; i < snapValidLinks.snapshotLength; i++) {
				var elmInvalid = snapValidLinks.snapshotItem(i);
				elmInvalid.title = 'This page claimed to validate, but it lied';
				elmInvalid.innerHTML = 'Invalid markup!';
			}
		}
	})

Running the Hack

Before installing the user script, go to http://www.matrix.msu.edu and look in the pane on the right for the "W3C XHTML 1.0" badge. Then, install the user script from Tools → Install This User Script, and refresh the MATRIX home page. After a second or two, the "W3C XHTML 1.0" badge will be replaced by a link that reads "Invalid markup!," as shown in Figure 12-5. Clicking the link confirms that there are numerous markup errors on the page.

Figure 12-5. Showing that MATRIX doesn't use valid XHTML 1.0, though they claim to

Showing that MATRIX doesn't use valid XHTML 1.0, though they claim to

Animate Wikipedia History

Watch a full-screen timeline of how a Wikipedia page evolved.

The fundamentally fascinating thing about Wikipedia is that is can be edited by anyone. If you see a mistake, you can correct it. If you know something more about a topic, you can add it. If you think an image would be helpful as a reference, you can upload it. And, of course, if you're just a jackass who likes to destroy other people's work, you can deface it.

All of these actions are recorded, and you can roll back the clock to see what a page looked like at a specific revision. This hack takes this revision history one step further by constructing an animated timeline of the life of a Wikipedia entry, from its inception to its current state.

The Code

This user script runs on all Wikipedia history pages. It adds an "Animate changes" button to the history page that acts as the main entry point for the rest of the script. The animation itself is a series of calls to Wikipedia's revision history interface.

We create an XMLHttpRequest object to retrieve the actual revision text and associated metadata (such as the author and revision date). The script also constructs a slider (really, a styled <div> with appropriate styling and event handlers) that tracks the current status of the animation, from the first version of the page to the current revision.

Save the following user script as wikipedia-animate.user.js:

	// ==UserScript==
	// @name			Wikipedia Animate
	// @namespace		http://phiffer.org/greasemonkey/
	// @description		Animates page modifications between two specific edit points
	// @include			http://*.wikipedia.tld/*action=history*
	// ==/UserScript==

	// based on code by Dan Phiffer
	// and included here with his gracious permission

	function Animation() {

		if (!document.getElementById('bodyContent')) {
			return;
		}

		var url = window.location.href;
		this.base_url = url.substr(0, url.indexOf('&'));
		this.hostname = url.substr(7, url.indexOf('/', 8) - 7);

		this.add_buttons();
		this.add_options();
		this.add_css();

	}
	Animation.prototype.add_buttons = function() {
		// Create the animate buttons
		var button1 = document.createElement('input');
		button1.className = 'historysubmit';
		button1.style.marginLeft = '5px';
		button1.setAttribute('type', 'button');
		button1.value = 'Animate changes';
		button1.addEventListener('click', function() { animate.start(); },
	true); 
		button1.setAttribute('id', 'animate_button1');

		var button2 = button1.cloneNode(true);    
		button2.addEventListener('click', function() { animate.start(); }, 
	true); 
		button2.setAttribute('id', 'animate_button2');

		// Add the buttons to the page
		var history = document.getElementById('pagehistory');
		history.parentNode.insertBefore(button1, history);
		history.parentNode.appendChild(document.createTextNode(' '));
		history.parentNode.appendChild(button2);

	}
	
	Animation.prototype.add_options = function() {
	
		// Create the options box
		var toolbox = document.getElementById('p-tb');
		var options = document.createElement('div');
		options.className = 'portlet';

		options.innerHTML = '<h5>animate options</h5><div class="pBody"><ul>' +

		// Range selection
		'<li>Animate over:' +
		'<div><input type="radio" name="animate_range" id="animate_range_
	selected" value="selected" checked="checked"/> Selected</div>' + 
		'<div><input type="radio" name="animate_range" id="animate_range_all"
	value="all"/> All versions</div>' +
		'<div><input type="checkbox" id="animate_skip_minor"/> Skip minor
	edits</div>' +

		// Diffs
		'</li><li>Highlight diffs:' +
		'<div><input type="radio" name="animate_diff" id="animate_diff_yes"
	value="yes" checked="checked"/> Yes</div>' +
		'<div><input type="radio" name="animate_diff" id="animate_diff_no"
	value="no"/> No</div>' +

		// Speed
		'</li><li>Animate speed:' +
		'<div>Pause <input type="text" id="animate_delay" value="0.5" size="3"
	style="font-size: 10px" onblur="animate.option(this);"/> sec</div>' +
		// Info
		'</li><li>Include info:' +
		'<div><input type="checkbox" id="animate_info_date" checked="checked"/>
	Date/time</div>' +
		'<div><input type="checkbox" id="animate_info_author" checked="checked"/
	> Author</div>' +
		'<div><input type="checkbox" id="animate_info_summary"
	checked="checked"/> Change summary</div>' +
		'</li></ul></div>';

		toolbox.parentNode.appendChild(options);
	}

	Animation.prototype.add_css = function() {

		// Add some CSS formatting rules for diffs
		var head = document.getElementsByTagName('head')[0];
		var style = document.createElement('style');
		style.type = 'text/css';
		style.innerHTML = 'ins.diff { display: inline; background-color: #CFC;
	font-weight: bold; } ' +
		'del.diff { background-color: #FFA; display: inline; } ' +
		'#animate_main { width: 100%; position: relative; } ' +
		'#animate_controls { position: absolute; top: 0; left: 0; background:
	transparent url(data:image/
	png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAA8CAYAAACuGnCAAAAABGdBTUEAANbY1E9YM
	gAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAABnSURBVHjaYvz///
	9NBjTAxIAFjAqOCo4KDl7Bf0D8H4rBgAWIvwExIxIGC36G6oBLgAQ/oakEC75HspAJJvgOm/
	a3uAQZkSSY8KrEEES2iAnm+I/
	Y3PkJ6ka4ICOwWOOA+RkmCBBgAIPPFd35TefZAAAAAElFTkSuQmCC) repeat-x; width:
	100%; } ' +
		'#animate_controls span.text { background: #FFF; } ' +
		'#animate_main div.content { position: absolute; top: 60px; display:
	none; } ' +
		'#animate_button { float: left; margin-left: 0; margin-top: 5px; width:
	50px; } ' +
		'#animate_scrubber { position: relative; width: 402px; height: 11px;
	border: 1px solid #AAA; background: #F5F5F5; float: left; margin: 8px; } ' +
		'#animate_load_progress { position: absolute; top: 1px; left: 1px;
	background: #E1E1E1; height: 9px; width: 5px; visibility: hidden; } ' +
		'#animate_playhead { position: absolute; left: 1px; top: 1px; cursor:
	pointer; background: transparent url(data:image/
	gif;base64,R0lGODlhCQAJAIAAAP///
	wAAACH5BAEAAAAALAAAAAAJAAkAAAIRhBGnwYrcDJxvwkplPtchVQAAOw==) no-repeat;
	height: 9px; width: 9px; } ' +
		'#animate_status { font: 10px verdana, sans-serif; float: left; margin-top:
	8px; } ' +
		'#animate_info { font-size: 10px; margin: 0 0 20px 58px; }';
		head.appendChild(style);
	}
	Animation.prototype.start = function() {
		// Initialize variables
		this.urls = new Array();
		this.info = new Array();
		this.pages = new Array();
		this.activity = new Array();
		this.activity_max = 0;
		this.num_loaded = 0;
		this.pos = 0;	
		this.interval = -1;
		this.prev = -1;
		this.status = 1; /* Status codes:
				0: history
				1: loading
				2: playing
				3: paused
				4: playhead scrub */
		var history = document.getElementById('pagehistory');
		var items = history.getElementsByTagName('li');

		// Cache the current history view
		var bodyContent = document.getElementById('bodyContent');
		this.history_content = this.mediawiki_content(bodyContent.innerHTML);

		// Check whether to animate over all article revisions
		if (document.getElementById('animate_range_all').checked) {

			// Check to see if the current history page already contains every
	revision
			var last_row = items[items.length - 1];
			var last_links = last_row.getElementsByTagName('a');
			var first_row = items[0];
			var first_links = first_row.getElementsByTagName('a');

			// The first and last list items each lack a 'last' and 'cur' link,
	respectively
			if (last_links[1].firstChild.nodeValue == 'last' ||
				first_links[0].firstChild.nodeValue == 'cur') {
				this.get_full_history();
				return;
			} else {
				first_row.getElementsByTagName('input')[1].checked = true;
				last_row.getElementsByTagName('input')[0].checked = true;
			}
		}
		this.parse_history();
		this.setup_markup();
		this.start_loading();
	}
	Animation.prototype.get_full_history = function() {

		// Disable the animate buttons while we load
		var button1 = document.getElementById('animate_button1');
		button1.value = 'Loading…';
		button1.setAttribute('disabled', 'disabled');

		var button2 = document.getElementById('animate_button2');
		button2.value = 'Loading…';
		button2.setAttribute('disabled', 'disabled');

		// Load in the full history
		var request = new XMLHttpRequest();
		request.open('GET', this.base_url +
	'&action=history&limit=5000&offset=0', true);
		request.onreadystatechange = function() {
			if (request.readyState == 4) {

				var content = animate.mediawiki_content(request.responseText);
				document.getElementById('bodyContent').innerHTML = content;

				var history = document.getElementById('pagehistory');
				var items = history.getElementsByTagName('li');
				var inputs = items[items.length - 1].
	getElementsByTagName('input');
				inputs[0].checked = true;

				animate.parse_history();
				animate.setup_markup();
				animate.start_loading();
			}
		}
		request.send(null);
	}

	Animation.prototype.parse_history = function() {
	
	var history = document.getElementById('pagehistory');
	var items = history.getElementsByTagName('li');
	var skip_minor = document.getElementById('animate_skip_minor').checked;
	var found_start = false;

	for (var i = 0; i < items.length; i++) {

		var radios = items[i].getElementsByTagName('input');
		var skip = false;
		
		// Skip this revision if it's been labeled 'minor'
		if (skip_minor) {
			var spans = items[i].getElementsByTagName('span');
			for (var j = 0; j < spans.length; j++) {
				if (spans[j].className == 'minor') {
				skip = true;
				}	
				}
			}

			if (radios[1] && radios[1].checked) {
				var links = items[i].getElementsByTagName('a');
				if (links[0].firstChild.nodeValue != 'cur' && !skip) {
				this.urls.unshift(links[1].getAttribute('href'));
				this.info.unshift(this.parse_info(items[i], 1));
				} else if (!skip) {
				this.urls.unshift(links[2].getAttribute('href'));
				this.info.unshift(this.parse_info(items[i], 2));
				}
				found_start = true;
			} else if (radios[0] && radios[0].checked) {
				var links = items[i].getElementsByTagName('a');
				if (links[1].firstChild.nodeValue != 'last' && !skip) {
				this.urls.unshift(links[1].getAttribute('href'));
				this.info.unshift(this.parse_info(items[i], 1));
				} else if (!skip) {
				this.urls.unshift(links[2].getAttribute('href'));
				this.info.unshift(this.parse_info(items[i], 2));
				}
				break;
			} else if (found_start && !skip) {
				var links = items[i].getElementsByTagName('a');
				this.urls.unshift(links[2].getAttribute('href'));
				this.info.unshift(this.parse_info(items[i], 2));
			}
		}
	}
	
	Animation.prototype.setup_markup = function() {
	
		this.add_nav_link();
	
		var content = '<div id="animate_main">' +
		'<div id="animate_controls">' +
		'<input type="button" value="Pause" class="historysubmit" id="animate_
	button"/> ' +
		'<div id="animate_scrubber">' +
		'<div id="animate_load_progress"></div>' +
		'<div id="animate_playhead"></div></div>' +
		'<div id="animate_status">Loading…</div>' +
		'<br style="clear: both;"/>' +
		'<div id="animate_info"></div>' +
		'</div></div>';
		document.getElementById('bodyContent').innerHTML = content;
		document.getElementById('bodyContent').style.height = '250px';

		this.content = 1;
		var playhead = document.getElementById('animate_playhead');
		playhead.addEventListener('mousedown', function(e) {
			animate.playhead(e); e.preventDefault();
		}, true);

		var button = document.getElementById('animate_button');
		button.addEventListener('click', function() {
			animate.button();
		}, true);

		var body = document.getElementsByTagName('body').item(0);
		body.addEventListener('mouseup', function(e) {
			animate.mouseup(e);
		}, true);
		body.addEventListener('mousemove', function(e) {
			animate.mousemove(e);
		}, true);

		var top = 0;
		var curr = document.getElementById('animate_main');
		while (curr.offsetParent) {
			top += curr.offsetTop;
			curr = curr.offsetParent;
		}
		this.scroll_origin = top;

		window.setInterval(function() { animate.check_scroll(); }, 50);
	}

	Animation.prototype.add_nav_link = function() {
		var history_nav = document.getElementById('ca-history');
		history_nav.className = '';
		history_nav.getElementsByTagName('a').item(0).addEventListener(
			'click', function(event) {
			animate.status = 0;
			document.getElementById('bodyContent').innerHTML = animate.history_
	content;
			histrowinit();
			this.parentNode.className = 'selected';

			var animate_nav = document.getElementById('animate_nav');
			this.parentNode.parentNode.removeChild(animate_nav);
			document.getElementById('animate_button1').addEventListener(
				'click', function() { animate.start(); }, true);
			document.getElementById('animate_button2').addEventListener(
				'click', function() { animate.start(); }, true);
			event.preventDefault();
		}, true);

		var animate_nav = document.createElement('li');
		var link = animate_nav.appendChild(document.createElement('a'));
		animate_nav.id = 'animate_nav';
		link.appendChild(document.createTextNode('animate'));

		link.setAttribute('href', '#');
		link.addEventListener('click', function(event) {
			event.preventDefault();
		}, true);
		animate_nav.className = 'selected';
		history_nav.parentNode.appendChild(animate_nav);
	}
	Animation.prototype.start_loading = function() {
		var url = 'http://' + this.hostname + this.urls[0];

		var request = new XMLHttpRequest();
		request.open('GET', url, true);
		request.onreadystatechange = function() {
			if (request.readyState == 4) {
				animate.loaded(request);
			}
		}
		request.send(null);
	}
	Animation.prototype.parse_info = function(item, l) {
		var info = '';
		var links = item.getElementsByTagName('a');

		if (document.getElementById('animate_info_date').checked) {
			var href = links.item(l).getAttribute('href');
			var text = links.item(l).firstChild.nodeValue;
			info += '<a href="' + href + '">' + text + '</a> ';
		}

		if (document.getElementById('animate_info_author').checked) {
			var href = links.item(l + 1).getAttribute('href');
			var text = links.item(l + 1).firstChild.nodeValue;
			info += 'by <a href="' + href + '">' + text + '</a> ';
		}
		if (document.getElementById('animate_info_summary').checked) {
			var em = item.getElementsByTagName('em');
			if (em.length == 1) {
				info += '&nbsp;&nbsp;&nbsp;' + em.item(0).innerHTML;
			}
		}

		info = '<span class="text">' + info + '</span>';

		return info;
	}

	Animation.prototype.loaded = function(details) {
		var content = this.mediawiki_content(details.responseText);
		this.pages[this.pages.length] = content;

		if (this.num_loaded > 0 && document.getElementById('animate_diff_yes').
		checked) {

			content = diffString(this.pages[this.num_loaded - 1], content);
		}
		var frame = document.createElement('div');
		var main = document.getElementById('animate_main');
		var controls = document.getElementById('animate_controls');

		main.insertBefore(frame, controls);
		frame.innerHTML = content;
		frame.className = 'content';
		frame.setAttribute('id', 'frame' + this.num_loaded);
		var load_progress = document.getElementById('animate_load_progress');
		load_progress.style.width = 5 + (395 * this.num_loaded / (this.urls.
	length - 1)) + 'px';
		load_progress.style.visibility = 'visible';
		if (this.num_loaded > 0 && document.getElementById('animate_diff_yes').
	checked) {

			var activity = 0;
			
			// Check for added content
			var b_list = frame.getElementsByTagName('b');
			for (var i = 0; i < b_list.length; i++) {
				if (b_list[i].className == 'diff') {
				activity++;
				}
			}

			// Check for deleted content
			var s_list = frame.getElementsByTagName('s');
			for (var i = 0; i < s_list.length; i++) {
				if (s_list[i].className == 'diff') {
				activity++;
				}
			}
			var id = this.activity.length;
			this.activity[id] = activity;

			var a = document.createElement('div');
			document.getElementById('animate_load_progress').appendChild(a);
			a.setAttribute('id', 'animate_activity' + id);
			a.style.position = 'absolute';
			a.style.left = (395 * (this.num_loaded - 1) / (this.urls.length -
		1)) + 'px';
			a.style.width = (395 / (this.urls.length - 1)) + 'px';

			if (this.num_loaded == 1) {
				a.style.width = parseFloat(a.style.width) + 5 + 'px';
			} else {
				a.style.left = parseFloat(a.style.left) + 5 + 'px';
			}

			a.style.height = '9px';
			a.style.top = '0px';


			if (this.activity_max == 0) {
				if (activity == 0) {
				var digit = 225;
				} else {
				this.activity_max = activity;
				var digit = 153;
				}
				a.style.background = 'rgb(' + digit + ',' + digit + ',' + digit
	+ ')';
			} else {
				if (activity > this.activity_max) {
				this.activity_max = activity;
				this.normalize_activity();
				} else {
				var digit = parseInt(225 - 72 * activity / this.activity_
	max);
				a.style.background = 'rgb(' + digit + ',' + digit + ',' +
	digit + ')';
				}	
			}

		}

		if (this.status == 1) {
			this.swap_content(this.num_loaded);
			var playhead = document.getElementById('animate_playhead');
			playhead.style.left = 1 + (390 * this.num_loaded / (this.urls.length
	- 1)) + 'px';
			this.set_info(this.num_loaded);
			this.pos = this.num_loaded;
		
		}

		this.num_loaded++;

		if (this.num_loaded < this.urls.length &&
			this.status != 0) {
			var url = 'http://' + this.hostname + this.urls[this.num_loaded];

			var request = new XMLHttpRequest();
			request.open('GET', url, true);
			var _this = this;
			request.onreadystatechange = function() {
				if (request.readyState == 4) {
				_this.loaded(request);
				}
			}	
			request.send(null);

		} else if (this.num_loaded == this.urls.length) {
			this.pause();
		}
	}

	Animation.prototype.normalize_activity = function() {
		for (var i = 0; i < this.activity.length; i++) {
			var a = document.getElementById('animate_activity' + i);
			var digit = parseInt(225 - 72 * this.activity[i] / this.activity_
	max);
			a.style.background = 'rgb(' + digit + ',' + digit + ',' + digit +
	')';
		}
	}

	Animation.prototype.button = function() {
		if (this.status == 3) {
			this.play();
		} else {
			this.pause();
		}
	}
	
	Animation.prototype.play = function() {
		
		this.status = 2;
		var button = document.getElementById('animate_button').value = 'Pause';

		if (this.pos + 1 == this.urls.length) {
			var playhead = document.getElementById('animate_playhead');
			playhead.style.left = '1px';
			this.pos = 0;
		}

		this.show_frame(this.pos);

		var delay = Math.round(parseFloat(document.getElementById('animate_
	delay').value) * 1000);
		this.interval = window.setInterval(function() { animate.show_frame();
	}, delay);
	}

	Animation.prototype.pause = function() {

		this.status = 3;
		var button = document.getElementById('animate_button').value = 'Play';

		if (this.interval != -1) {
			clearInterval(this.interval);
		}
	}

	Animation.prototype.show_frame = function(num) {
	
		if (this.status == 0 || this.status == 3) {
			return;

		}
		
		// If not scrubbing
		if (this.status != 4) {
			var num = this.pos;
			var playhead = document.getElementById('animate_playhead');
			playhead.style.left = 1 + (390 * num / (this.urls.length - 1)) +
	'px';
		}
		this.swap_content(num);
		this.set_info(num);

		if (this.status == 2) {
			if (this.pos + 1 >= this.pages.length) {
				this.pause();
			} else {
				this.pos++;
			}
		}
	}		

	Animation.prototype.set_info = function(num) {
		
		document.getElementById('animate_info').innerHTML = this.info[num];


		var prev = (num > 0) ? '<a href="#" onclick="animate.prev_frame();
	return false;" accesskey="p">&larr;</a> ' : '&larr; ';
		var frame = (num + 1) + ' / ' + this.urls.length;
		var next = (num < this.urls.length - 1) ? ' <a href="#"
	onclick="animate.next_frame(); return false;" accesskey="nw">&rarr; </a>' : '
	&rarr;';
		document.getElementById('animate_status').innerHTML = '<span
	class="text">' + prev + frame + next + '</span>';

	}

	Animation.prototype.playhead = function(e) {
		this.status = 4;
		return false;
	}

	Animation.prototype.prev_frame = function() {
		this.pos--;
		this.show_frame(this.pos);
		this.status = 3;
	}
	
	Animation.prototype.next_frame = function() {
		if (this.pos + 1 < this.num_loaded) {
			this.pos++;
			this.show_frame(this.pos);

			this.status = 3;
		}
	}

	Animation.prototype.mousemove = function(e) {

		// Make sure the user has clicked on the playhead
		if (animate.status != 4) {
			return;
		}

		var scrubber = document.getElementById('animate_scrubber');
		var left = 0;
		var curr = scrubber;
		while (curr.offsetParent) {
			left += curr.offsetLeft;
			curr = curr.offsetParent;
		}

		var playhead = document.getElementById('animate_playhead');
		var x = e.pageX - left - 5;

		if (x > 391) {
			x = 391;
		} else if (x < 1) {
			x = 1;
		}

		var load_progress = document.getElementById('animate_load_progress');
		if (x > parseInt(load_progress.style.width - 5)) {
			x = parseInt(load_progress.style.width - 5);
		}

		playhead.style.left = x + 'px';

		var snap = Math.floor((x - 1) * (animate.urls.length - 1) / 390);
		if (snap != animate.pos) {
			animate.pos = snap;
			animate.show_frame(snap);
		}
	}
	
	Animation.prototype.mouseup = function(e) {

		if (animate.status != 4) {
			return;
		}

		var scrubber = document.getElementById('animate_scrubber');
		var left = 0;
		var curr = scrubber;
		while (curr.offsetParent) {

			left += curr.offsetLeft;
			curr = curr.offsetParent;
		}

		var playhead = document.getElementById('animate_playhead');
		var x = e.pageX - left - 5;
	
		if (x > 391) {
			x = 391;
		} else if (x < 1) {
			x = 1;
		}

		var load_progress = document.getElementById('animate_load_progress');
		if (x > parseInt(load_progress.style.width)) {
			x = parseInt(load_progress.style.width);
		}

		var snap = Math.floor((x - 1) * (animate.urls.length - 1) / 390);
		if (snap != animate.pos) {
			animate.pos = snap;
			animate.show_frame(snap);
		}
		animate.status = 3;

	}

	Animation.prototype.option = function(input) {

	}

	Animation.prototype.swap_content = function(num) {
	
		var frame = document.getElementById('frame' + num);
		frame.style.display = 'block';

		var height = parseInt(frame.offsetHeight) + 60;
		document.getElementById('bodyContent').style.height = height + 'px';

		if (this.prev != -1) {
			var prev = document.getElementById('frame' + this.prev);
			prev.style.display = 'none';
		}

		this.prev = num;

	}

	Animation.prototype.mediawiki_content = function(text) {
		text = '' + text;
		var start = text.indexOf('<!-- start content -->');
		var end = text.indexOf('<!-- end content -->');
		return text.substr(start, end - start);
	}

	Animation.prototype.check_scroll = function() {
		var controls = document.getElementById('animate_controls');
		if (self.pageYOffset > this.scroll_origin) {
			controls.style.top = (self.pageYOffset - this.scroll_origin) + 'px';
		} else {
			controls.style.top = 0;
		}
	}

	var animate = new Animation();

	// JavaScript diff code thanks to John Resig (http://ejohn.org)
	// http://ejohn.org/files/jsdiff.js
	function diffString( o, n ) {
		var out = diff( o.split(/\s+/), n.split(/\s+/) );
		var str = "";

		for ( var i = 0; i < out.n.length - 1; i++ ) {
			if ( out.n[i].text == null ) {
				if ( out.n[i].indexOf('"') == -1 && out.n[i].indexOf('<') == -1
	&& out.n[i].indexOf('=') == -1 ) {
				str += "<b style='background:#E6FFE6;' class='diff'> " +
	out.n[i] +"</b>";
				} else {
				str += " " + out.n[i];
				}
			} else {
				var pre = "";
				if ( out.n[i].text.indexOf('"') == -1 && out.n[i].text.
	indexOf('<') == -1 && out.n[i].text.indexOf('=') == -1 ) {
				var n = out.n[i].row + 1;
				while ( n < out.o.length && out.o[n].text == null ) {
				if ( out.o[n].indexOf('"') == -1 && out.o[n].
	indexOf('<') == -1 && out.o[n].indexOf(':') == -1 && out.o[n].indexOf(';')
	== -1 && out.o[n].indexOf('=') == -1 ) {
				pre += " <s style='background:#FFE6E6;'
	class='diff'>" + out.o[n] +" </s>";
				}
				n++;
				}
				}
				str += " " + out.n[i].text + pre;
			}
		}

		return str;
	}
	function diff( o, n ) {
		var ns = new Array();

		var os = new Array();

		for ( var i = 0; i < n.length; i++ ) {
			if ( ns[ n[i] ] == null ) {
				ns[ n[i] ] = { rows: new Array(), o: null };
			}
			if (ns[n[i]].rows) {
				ns[ n[i] ].rows.push( i );
			}
		}
		
		for ( var i = 0; i < o.length; i++ ) {
			if ( os[ o[i] ] == null ) {
				os[ o[i] ] = { rows: new Array(), n: null };
			}
			if (os[o[i]].rows) {
				os[ o[i] ].rows.push( i );
			}
		}
		
		for ( var i in ns ) {
			if ( ns[i].rows.length == 1 && typeof(os[i]) != "undefined" &&
		os[i].rows.length == 1 ) {
				n[ ns[i].rows[0] ] = { text: n[ ns[i].rows[0] ], row: os[i].
		rows[0] };
				o[ os[i].rows[0] ] = { text: o[ os[i].rows[0] ], row: ns[i].
		rows[0] };
				}
			}

			for ( var i = 0; i < n.length - 1; i++ ) {
				if ( n[i].text != null && n[i+1].text == null && o[ n[i].row + 1 ].
		text == null &&
				 n[i+1] == o[ n[i].row + 1 ] ) {
				n[i+1] = { text: n[i+1], row: n[i].row + 1 };
				o[n[i].row+1] = { text: o[n[i].row+1], row: i + 1 };
				}
			}

			for ( var i = n.length - 1; i > 0; i-- ) {
				if ( n[i].text != null && n[i-1].text == null && o[ n[i].row - 1 ].
		text == null &&
				 n[i-1] == o[ n[i].row - 1 ] ) {
				n[i-1] = { text: n[i-1], row: n[i].row - 1 };
				o[n[i].row-1] = { text: o[n[i].row-1], row: i - 1 };
				}
			}

			return { o: o, n: n };
		}

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://en.wikipedia.org/wiki/Heavy_metal_umlaut and click the History tab. You will see a new "Animate changes" button, as shown in Figure 12-6.

Figure 12-6. Wikipedia "Animate changes" button

Wikipedia "Animate changes" button

Select two revisions from the History page, and then click the "Animate changes" button. The script will go to work, fetching the selected revisions one by one. This might take some time, because Wikipedia is not optimized for fetching older revisions of a page (since this is not a frequent operation). As the script loads each revision, it displays the page as it once appeared. A slider at the top of the page shows the current progress, as shown in Figure 12-7.

The script adds an options panel on the left side of the page. You can skip edits that were marked as minor when the author made the revision. You can set the delay between revisions, although I have not found this to be terribly useful, since Wikipedia is so slow to begin with. You can include metadata such as the revision date, the author (if the person who made the change had logged into Wikipedia), and the summary the author entered when he made the revision.

Figure 12-7. Animation progress

Animation progress

You can also show differences between each revision by highlighting added, removed, and modified text with separate colors.

The script caches revisions as it fetches them. Once it has retrieved a few revisions, you can move the slider back or forward to quickly jump between the cached revisions. This is a fascinating effect; sadly, a single screenshot cannot do it justice. Try it for yourself, and watch as people add information, deface the page, revert the vandalism, correct typos, add links, and generally evolve the page into what it has become today.

Create Greasemonkey Scripts Automatically

Meet Platypus, the graphical interface for Greasemonkey.

By this point, you have seen the awesome power of Greasemonkey to modify pages in myriad ways. But all these hacks have one thing in common: they require writing code. Sometimes that's unavoidable, but wouldn't it be nice to be able to make simple modifications without poking through the DOM or futzing with XPath?

Meet Platypus, the graphical interface for Greasemonkey.

Running the Hack

To install Platypus, go to http://platypus.mozdev.org and click the Install Platypus link. Firefox uses a whitelist to restrict sites from automatically installing browser extensions. If you see a message at the top of the window that says that Firefox has prevented this site from installing software, click Edit Options and add the site to your whitelist, as shown in Figure 12-8.

Figure 12-8. Firefox extensions whitelist

Firefox extensions whitelist

Now, click Install Platypus again, and Firefox will prompt you to confirm that you really want to install it. Click Install, and then quit Firefox and relaunch it to complete the installation.

OK, now you should see a new option in the Tools menu named "Platypus!" Go to any web page, such as http://www.oreilly.com, and then select "Platypus!" from the Tools menu. Since this is the first time you have run Platypus, it will open its help window to show you all the available options, as shown in Figure 12-9.

Close the help window, and you can start making modifications to the loaded page. As you move the cursor around the page, Platypus will highlight individual elements in red, as shown in Figure 12-10.

Figure 12-9. Platypus help window

Platypus help window

Figure 12-10. Element highlighted by Platypus

Element highlighted by Platypus

What can you do with highlighted elements? All kinds of fun stuff. Press R to relax the element, removing any fixed size and positioning. Press C to center it, or B to modify it to black text on a white background. Press E to erase it (leaving its space intact), or X to delete it entirely (collapsing other elements around it).

You can even move elements around. Press Ctrl-X to cut the element, and then move to another position in the page and press Ctrl-V to paste it.

Is that still not enough control for you? Highlight any element and press S to bring up its style properties, as shown in Figure 12-11.

Figure 12-11. Edit Content Style dialog

Edit Content Style dialog

In this dialog, you can change the element's color, dimensions, border, and font properties to anything you like.

Still not satisfied? Highlight an element and press V to view its HTML source in another window, or press M to modify its HTML markup. Or position the cursor anywhere and press H to insert arbitrary HTML into the page at that point.

If this is all too much work for you, you can load any page and press A to autorepair the page. This finds the biggest element on the page, removes everything else, widens the element to span the entire browser window, removes any background images, and sets the text to black on a white background. It doesn't work on every page, but it is especially useful on busy news article pages where you need to fight the distractions of the navigation bar, the sidebars, the towering ad banners, and the cluttered page footer to get to the actual content of the article.

So what does all this have to do with Greasemonkey? Once you've finished your modifications and you're happy with the page, press Ctrl-S to save your modifications as a Greasemonkey script. Platypus will even pop up the standard Greasemonkey install dialog and automatically install it for you, as shown in Figure 12-12.

Figure 12-12. Saving a Platypus mod as a Greasemonkey script

Saving a Platypus mod as a Greasemonkey script

Once it's installed, you can open the Manage User Scripts dialog from the Tools menu, select the script, and click Edit to see how it works. Platypus exposes API functions for each of the modifications you made by pointing and clicking. The autogenerated Greasemonkey script calls Platypus API functions to replay the modifications every time you visit the page. This means that these Greasemonkey scripts will work only as long as you have Platypus installed.

Remember Everything You Read

Create a personal command line for the Web.

This hack holds a special place in my heart. It is everything I have always wanted a browser to be: a personal command line for the Web: my Web.

Firefox keeps track of pages you visit, and you can revisit them later by browsing the History window. But the Web I use is so much more than just URLs and page titles. I browse weblogs that syndicate their content through RSS and Atom feeds. I visit personal home pages of people that have FOAF files. (FOAF stands for Friend of a Friend and is an RDF vocabulary for expressing personal information and relationships.) I read articles that the author has taken the time to tag or categorize with keywords.

My Web is full of metadata. And this metadata is more memorable to me than a URL. If I read an article on Monday morning about a cool CSS hack or an upcoming conference, by Wednesday afternoon, I've long since closed the window and forgotten where I read it or what it was titled. I remember that the author tagged it with css or oreilly or syndication, but that's it. What can I do?

This hack is with me everywhere I go. As I'm reading, it is quietly collecting all the metadata it can find: title, URL, referrer, tags and keywords, RSS and Atom feeds, and FOAF files. On Wednesday afternoon, when I want to find that one specific article again, I can press a hotkey and bring up Magic Line: my personal command line. I type a few letters, and Magic Line autocompletes my thought before I can even finish the first word, culling through the hundreds of pages I've visited recently to pull up exactly the article I was thinking of.

Magic.

The Code

This user script runs on all pages. It is very long, but it breaks down into six sections:

Utility functions
I hate that JavaScript strings don't have a startswith method. I hate that arrays don't have a contains method. Too many years programming in Python have spoiled me. The first few functions are just utility functions that make JavaScript behave the way I want it to, so I can spend the rest of the script writing code that comes naturally.
Feed parser
The PoorMansFeedParser is a quick-and-dirty parser for RSS and Atom feeds. Magic Line autodiscovers the feed for the current page by looking for specially marked <link> elements, and parses all the titles, links, and descriptions from the feed. This means I can find a page later that I never even visited. All I need to do is browse the front page of a weblog, and all the recent articles are added to my personal data store. (Magic Line caches feeds for 24 hours to avoid pummeling the server with every page view.)
FOAF parser
The FOAF RDF vocabulary is like a machine-readable about page. People can publish their name, biography, projects, and other personal information in a standard format that is easily parseable. The PoorMansFOAFParser mines FOAF files for metadata. This means I can find someone by name, even if I forget the URL. FOAF files are also autodiscoverable, with <link> elements in the <head> of the web page.
Parsing other page metadata
Author and keyword information can be hidden in <meta> elements. Tags and category keywords can be marked with <a rel="tag">. Links to friends can be marked with <a rel="friend"> (and lots of other variations). And of course there's always the page title, the page URL, and the referring page URL.
User interface
The Magic Line user interface (magicShow, magicHide, magicKeypress, magicSubmit, magicScrollResults, and magicHideResults) literally takes over the entire browser window and sets up a simple form for finding any page, based on any scrap of information we were able to discover.
Autocompletion
The magicSearch function does the actual autocompletion matching, searching through all the metadata in our private data store and prioritizing the best matches. Page URLs take precedence over other types of metadata, followed by page titles, people's names, article descriptions, tags and keywords, and referrer URLs.

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

	// ==UserScript==
	// @name			Magic Line
	// @namespace		http://diveintomark.org/projects/greasemonkey/
	// @description		The magic personal command line for the web
	// @include			*
	// ==/UserScript==

	var _onkeypress = null;
	var gURLs = [];
	var gSelectedIndex = 0;

	String.prototype.trim = function() {
		return this.replace(/^\s*(\S*(\s+\S+)*)\s*$/, "$1");
	}

	String.prototype.normalize = function() {
		return this.replace(/\s+/g, ' ').trim();
	}

	String.prototype.startswith = function(sMatch) {
		return this.indexOf(sMatch) == 0;
	}

	String.prototype.replaceString = function(sOld, sNew) {
		var re = '';
		var arSpecialChars = ['\\', '[', ']', '(', ')', '.', '*', '+',
				  '^', '$', '?', '|', '{', '}'];
		for (var i = 0; i < sOld.length; i++) {
		var c = sOld.charAt(i);
		if (arSpecialChars.contains(c)) {
			re += '\\' + c;
		} else {
			re += c;
		}
		}	
		var oRegExp = new RegExp('(' + re + ')', 'gim');
		return this.replace(oRegExp, sNew);
	}

	String.prototype.lpad = function(cPadder, iMaxLen) {
		var s = this;
		for (var i = s.length; i < iMaxLen; i++) {
		s = cPadder + s;
		}
		return s;
		}
	
	String.prototype.rpad = function(cPadder, iMaxLen) {
		var s = this;
		for (var i = s.length; i < iMaxLen; i++) {
		s = s + cPadder;
		}
		return s;
	}
	
	String.prototype.toAscii = function() {
		return this.replace(/[^a-zA-Z0-9\!\@\#\$\%\^\&\*\(\)\-\=\_\
				  \+\[\]\\\{\}\|\;\'\:\"\,\.\/\<\>\? ]/g, ' ');
	}

	String.prototype.containsAll = function(arKeywords) {
		var s = this.toLowerCase();
		for (var i = 0; i < arKeywords.length; i++) {
		var sKeyword = arKeywords[i].toLowerCase();
		if (s.indexOf(sKeyword) == -1) {
			return false;
		}
		}
		return true;
	}

	String.prototype.containsAny = function(arKeywords) {
		var s = this.toLowerCase();
		for (var i = 0; i < arKeywords.length; i++) {
		var sKeyword = arKeywords[i].toLowerCase();
		if (s.indexOf(sKeyword) != -1) {
			return true;
		}
		}
		return false;
	}

	String.prototype.toXML = function() {
		var oParser = new DOMParser();
		return oParser.parseFromString(this, 'application/xml');
	}

	Array.prototype.contains = function(sString) {
		for (var i = 0; i < this.length; i++) {
		if (this[i] == sString) {
			return true;
		}
		}
		return false;
	}

	XMLDocument.prototype.NSResolver = function(prefix) {
		return {
	'atom03': 'http://purl.org/atom/ns#',
	'atom10': 'http://www.w3.org/2005/Atom',
	'dc': 'http://purl.org/dc/elements/1.1/',
	'foaf': 'http://xmlns.com/foaf/0.1/',
	'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
	'rss09': 'http://my.netscape.com/rdf/simple/0.9/',
	'rss10': 'http://purl.org/rss/1.0/',
	'xhtml': 'http://www.w3.org/1999/xhtml'
		}[prefix];
	}

	XMLDocument.prototype.textOf = function(elmRoot, sXPath) {
		var elmTarget = document.evaluate(sXPath, elmRoot, this.NSResolver,
			XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;

		return (elmTarget?elmTarget.textContent:'').replace(/<\S[^>]*>/g,'');
	}

	XMLDocument.prototype.firstOf = function(elmRoot, sXPath) {
		return document.evaluate(sXPath, elmRoot, this.NSResolver,
			XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
	}

	XMLDocument.prototype.arrayOf = function(elmRoot, sXPath) {
		var snapResults = document.evaluate(sXPath, elmRoot, this.NSResolver,
			XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
		var arResults = [];
		for (var i = 0; i < snapResults.snapshotLength; i++) {
		arResults.push(snapResults.snapshotItem(i));
		}
		return arResults;
	}
	
	var PoorMansFeedParser = {
		// some day I'll port my Universal Feed Parser to Javascript…

		feed: "/atom03:feed | " +
			  "/atom10:feed | " +
			  "/rdf:RDF/rss10:channel | " +
			  "/rdf:RDF/rss09:channel | " +
			  "/rss/channel",

		entries: "/atom03:feed/atom03:entry | " +
				 "/atom10:feed/atom10:entry | " +
				 "/rdf:RDF/rss10:item | " +
				 "/rdf:RDF/rss09:item | " +
				 "/rss/channel/item",

		title: "./atom03:title | " +
			   "./atom10:title | " +
			   "./rss10:title | " +
			   "./rss09:title | " +
			   "./dc:title | " +
			   "./title",

		link: "./atom03:link[@rel='alternate']/@href | " +
			  "./atom10:link[@rel='alternate']/@href | " +
			  "./rss10:link | " +
			  "./rss09:link | " +
			  "./link",

	  description: "./atom03:Magic Linetagline | " +
				   "./atom10:tagline | " +
				   "./atom03:summary | " +
				   "./atom10:summary | " +
				   "./rss10:description | " +
				   "./rss09:description | " +

				   "./dc:description | " +
				   "./description",

	  keywords: "./dc:subject | " +
				"./category",

	  name: "./atom03:author/atom03:name | " +
			  "./atom10:author/atom10:name | " +
			  "./dc:creator | " +
			  "./dc:author | " +
			  "./dc:publisher | " +
			  "./dc:owner | " +
			  "./author | " +
			  "./managingEditor | " +
			  "./managingeditor | " +
			  "./webMaster | " +
			  "./webmaster",
			  
	  _parseElement: function(oDom, elmRoot) {
	  var oResults = {};
	  oResults.url = oDom.textOf(elmRoot, this.link);
	  oResults.title = oDom.textOf(elmRoot, this.title);
	  oResults.name = oDom.textOf(elmRoot, this.name);
	  oResults.description = oDom.textOf(elmRoot, this.description);
	  oResults.keywords = oDom.textOf(elmRoot, this.keywords);
	  return oResults;
	  },

	  parse: function(sFeed) {
	  var oResults = {feed: {}, entries: []};
	  var oDom = sFeed.toXML();
	  var elmFeed = oDom.firstOf(oDom, this.feed);
	  if (elmFeed) {
		  oResults.feed = this._parseElement(oDom, elmFeed);
	  }
	  var arEntries = oDom.arrayOf(oDom, this.entries);
	  for (var i = 0; i < arEntries.length; i++) {
		  var elmEntry = arEntries[i];
		  if (elmEntry) {
		  oResults.entries.push(this._parseElement(oDom, elmEntry));
		  }
	  }
	  return oResults;
	  },
	}
	var PoorMansFOAFParser = {
		person: "//foaf:Person",
		name: "./foaf:name",
		url: "./foaf:homepage/@rdf:resource",
		keywords: "./dc:subject",

		_parsePerson: function(oDom, elmRoot) {
		var oResults = {};
		return oResults;
		},

		parse: function(sFoaf) {
		var arResults = [];
		var oDom = sFoaf.toXML();
		var arPerson = oDom.arrayOf(oDom, this.person);
		for (var i = 0; i < arPerson.length; i++) {
			var elmPerson = arPerson[i];
			if (elmPerson) {
			var oPerson = {};
			oPerson.name = oDom.textOf(elmPerson, this.name);
			oPerson.url = oDom.textOf(elmPerson, this.url);
			oPerson.keywords = oDom.textOf(elmPerson, this.keywords);
			arResults.push(oPerson);
			}
		}
		return arResults;
		}	
	}

	function getPageFoaf() {
		var elmPossible = document.evaluate("//link[@rel][@type][@href]",
			document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
		var arFoaf = [];
		for (var i = 0; i < elmPossible.snapshotLength; i++) {
		var elm = elmPossible.snapshotItem(i);
		if (!elm.rel || !elm.type || !elm.href) { continue; }
		var sRel = elm.rel.toLowerCase().normalize();
		if ((sRel + ' ').indexOf('meta ') == -1) { continue; }
		var sType = elm.type.toLowerCase().trim();
		if (sType != 'application/rdf+xml') { continue; }
		var urlFoaf = elm.href.trim();
		arFoaf.push(urlFoaf);
		}
		return arFoaf;
	}

	function getPageFeeds() {
		var elmPossible = document.evaluate("//*[@rel][@type][@href]",
			document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
		var arFeeds = [];
		for (var i = 0; i < elmPossible.snapshotLength; i++) {
		var elm = elmPossible.snapshotItem(i);
		if (!elm.rel || !elm.type || !elm.href) { continue; }
		var sRel = elm.rel.toLowerCase().normalize();
		if ((sRel + ' ').indexOf('alternate ') == -1) { continue; }
		var sType = elm.type.toLowerCase().trim();
		if ((sType != 'application/rss+xml') &&
			(sType != 'application/atom+xml') &&
			(sType != 'text/xml')) { continue; }

		var urlFeed = elm.href.trim();
		arFeeds.push(urlFeed);
		}
		return arFeeds;
	}
	function saveInfo(iIndex, sURL, sTitle, sName, sDescription, sKeywords,
			  sReferrer, sSource) {
		sTitle = (sTitle || '').trim().toAscii();
		sName = (sName || '').trim().toAscii();
		sDescription = (sDescription || '').trim().toAscii();
		sKeywords = (sKeywords || '').replace(/,/g, ' ').normalize().toAscii();
		sReferrer = (sReferrer || '').trim().toAscii();
		GM_setValue(iIndex + '.url', sURL);
		GM_setValue(iIndex + '.title', sTitle);
		GM_setValue(iIndex + '.name', sName);
		GM_setValue(iIndex + '.description', sDescription);
		GM_setValue(iIndex + '.keywords', sKeywords);
		GM_setValue(iIndex + '.referrer', sReferrer);
		GM_setValue(iIndex + '.source', sSource);
		GM_setValue(iIndex + '.Magic Linemagicsearch',
			sURL.toLowerCase().substring(0, 255).rpad(' ', 255) + ' ' +
			sTitle.toLowerCase().substring(0, 255).rpad(' ', 255) + ' ' +
			sName.toLowerCase().substring(0, 255).rpad(' ', 255) + ' ' +
			sDescription.toLowerCase().substring(0, 255).rpad(' ', 255) + ' ' +
			sKeywords.toLowerCase().substring(0, 255).rpad(' ', 255) + ' ' +
			sReferrer.toLowerCase().substring(0, 255).rpad(' ', 255) + ' ' +
			sSource.toLowerCase().substring(0, 255).rpad(' ', 255));
	}

	function findHistory(sKeywords) {
		var sMatchKeywords, iIndex;
		for (iIndex = 0; sMatchKeywords = GM_getValue('history.' + iIndex +
		'.keywords', null);
		++iIndex) {
		if (sKeywords == sMatchKeywords) { break; }
		}
		return iIndex;
	}

	function findCacheURL(sURL) {
		var sMatchURL, iIndex;
		for (iIndex = 0; sMatchURL = GM_getValue('cache.' + iIndex + '.url',
				null);
			++iIndex) {
		if (sURL == sMatchURL) { break; }
		}
		return iIndex;
	}

	function findURL(sURL) {
		var sMatchURL, iIndex;
		for (iIndex = 0; sMatchURL = GM_getValue(iIndex + '.url', null);

			++iIndex) {
		if (sURL == sMatchURL) { break; }
		}
		return iIndex;
	}
	function collectPageInfo() {
		try {
		var sURL = window.top.location.href;
		} catch (e) {
		var sURL = document.location.href;
		}
		var sTitle = (window.top.document.title || '');
		try {
		sTitle = sTitle.valueOf();
		sTitle = new String(sTitle).trim();
		} catch (e) {
		sTitle = '';
		}
		var sName = '';
		var sDescription = '';
		var sKeywords = '';

		// if this page has an entry already, get its index; otherwise
		// we'll create a new entry at the next available index
		var iIndex = findURL(sURL);

		// collect keywords, descriptions, and authors from <meta> elements
		var snapMeta = document.evaluate("//meta", document, null,
			XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
		var arKeywords = ['keywords', 'dc.keywords'];
		var arDescription = ['description', 'dc.description'];
		var arName = ['author', 'creator', 'owner', 'dc.author',
			'dc.creator', 'dc.publisher'];
		var arTitle = ['title', 'dc.title'];
		for (var i = 0; i < snapMeta.snapshotLength; i++) {
		var elmMeta = snapMeta.snapshotItem(i);
		var sMetaName = elmMeta.getAttribute("name");
		if (!sMetaName) { continue; }
		sMetaName = sMetaName.toLowerCase().trim();
		var sContent = elmMeta.getAttribute("content");
		if (!sContent) { continue; }
		sContent = sContent.normalize() + ' ';
		if (arKeywords.contains(sMetaName) &&
			(sKeywords.indexOf(sContent.toLowerCase()) == -1)) {
			sKeywords += sContent.toLowerCase();
		} else if (arDescription.contains(sMetaName) &&
				(sDescription.indexOf(sContent) == -1)) {
			sDescription += sContent;
		} else if (arName.contains(sMetaName) &&
			   (sName.indexOf(sContent) == -1)) {
			sName += sContent;
		} else if (!sTitle && arTitle.contains(sMetaName)) {

			sTitle = sContent.trim();
		}
		}

		// collect keywords from <a rel="tag"> elements
		for (var i = 0; i < document.links.length; i++) {
		var elmLink = document.links[i];
		var sRel = (elmLink.rel || '').toLowerCase().normalize() + ' ';
		if (sRel.indexOf('tag ') != -1) {
			sKeywords += elmLink.textContent.normalize() + ' ';
		}
		}

		// save page info
		saveInfo(iIndex, sURL, sTitle, sName, sDescription,
			sKeywords, document.referrer, 'history');
	}

	function collectXFNInfo() {
		var sXFNRel = 'contact acquaintance friend met co-worker coworker ' +
		'colleague co-resident coresident neighbor child parent ' +
		'sibling brother sister spouse wife husband kin relative ' +
		'muse crush date sweetheart me ';
		var arXFN = [];
		for (var i = 0; i < document.links.length; i++) {
		var elmLink = document.links[i];
		var sRel = (elmLink.rel || '').toLowerCase().normalize();
		if (!sRel) { continue; }
		var arRel = sRel.split(' ');
		for (var j = 0; j < arRel.length; j++) {
			if (sXFNRel.indexOf(arRel[j] + ' ') != -1) {
			arXFN.push({
				url: elmLink.href,
				name: elmLink.textContent.normalize(),
				description: (elmLink.title || '').normalize(),
				keywords: sRel
				});
			break;
			}
		}
		}
		for (var i = 0; i < arXFN.length; i++) {
		var oXFN = arXFN[i];
		var iXFNIndex = findURL(oXFN.url);
		saveInfo(iXFNIndex, oXFN.url, '', oXFN.name,
				oXFN.description, oXFN.keywords, '', 'XFN');
		}
	}

	function collectFOAFInfo() {
		var arFoaf = getPageFoaf();
		for (var i = 0; i < arFoaf.length; i++) {
		var sFoafURL = arFoaf[i];

		var iCacheIndex = findCacheURL(sFoafURL);
		if (GM_getValue('cache.' + iCacheIndex + '.url')) { continue; }
		GM_setValue('cache.' + iCacheIndex + '.url', sFoafURL);
		GM_setValue('cache.' + iCacheIndex + '.date',
				(new Date()).toString());
		GM_xmlhttpRequest({
				method: 'GET',
				url: sFoafURL,
				onload: function(oResponseDetails) {
				var arPerson = PoorMansFOAFParser.parse(
				oResponseDetails.responseText);
			for (var i = 0; i < arPerson.length; i++) {
				var oPerson = arPerson[i];
				if (!oPerson.url) { continue; }
				var iPersonIndex = findURL(oPerson.url);
				saveInfo(iPersonIndex,
				 oPerson.url,
				 GM_getValue(iPersonIndex +
				 '.title', ''),
				 GM_getValue(iPersonIndex +
				 '.name', oPerson.name),
				 GM_getValue(iPersonIndex +
				 '.description', ''),
				 GM_getValue(iPersonIndex +
				 '.keywords', oPerson.keywords),
				 GM_getValue(iPersonIndex +
				 '.referrer', sFoafURL),
				 GM_getValue(iPersonIndex +
				 '.source', 'FOAF'));
			}
			}});
		}
	}

	function collectFeedInfo() {
		var arFeeds = getPageFeeds();
		for (var i = 0; i < arFeeds.length; i++) {
		var sFeedURL = arFeeds[i];
		var iCacheIndex = findCacheURL(sFeedURL);
		var lCache = Date.parse(GM_getValue(
				'cache.' + iCacheIndex + '.date', new Date(0).toString()));
		var lNow = new Date().getTime();
		if (lCache != 0) {
			if (lNow < lCache) { continue; }
			if (lNow - lCache < 86400000) { continue; } // 1 day
		}
		GM_setValue('cache.' + iCacheIndex + '.url', sFeedURL);
		var dateNow = new Date(0);
		dateNow.setTime(lNow);
		GM_setValue('cache.' + iCacheIndex + '.date', dateNow.toString());
		GM_xmlhttpRequest({
				method: 'GET',
				url: sFeedURL,

				onload: function(oResponseDetails) {
				var oData = PoorMansFeedParser.parse(
				oResponseDetails.responseText);
			var oFeed = oData.feed;
			if (oFeed.url) {
				var iFeedIndex = findURL(oFeed.url);
				saveInfo(iFeedIndex,
				 oFeed.url,
				 GM_getValue(iFeedIndex +
				 '.title', oFeed.title),
				 GM_getValue(iFeedIndex +
				 '.name', oFeed.name),
				 GM_getValue(iFeedIndex +
				 '.description', oFeed.description),
				 GM_getValue(iFeedIndex +
				 '.keywords', '') + ' ' +
					 oFeed.keywords,
				 GM_getValue(iFeedIndex +
				 '.referrer', ''),
				 GM_getValue(iFeedIndex +
				 '.source', 'feed'));
			}
			var arEntries = oData.entries;
			if (arEntries.length) {
				for (var i = 0; i < arEntries.length; i++) {
				var oEntry = arEntries[i];
				if (!oEntry.url) { continue; }
				var iEntryIndex = findURL(oEntry.url);
				saveInfo(iEntryIndex,
				 oEntry.url,
				 GM_getValue(iEntryIndex +
				 '.title', oEntry.title),
				 GM_getValue(iEntryIndex +
				 '.name', oEntry.name),
				 GM_getValue(iEntryIndex +
				 '.description',
				 oEntry.description),
				 GM_getValue(iEntryIndex +
				 '.keywords', '') + ' ' +
				         oEntry.keywords,
				 GM_getValue(iEntryIndex +
				 '.referrer', ''),
				 GM_getValue(iEntryIndex +
				 '.source', 'feed'));
				}
			}
			}});
		}
	}

	function displayKeyFromEvent(e) {
		var bCtrlKey = e.ctrlKey;
		var bAltKey = e.altKey;

		var bShiftKey = e.shiftKey;
		var sDisplayKey = (bCtrlKey ? 'Ctrl + ' : '') +
		(bAltKey ? 'Alt + ' : '') +
		(bShiftKey ? 'Shift + ' : '');
		var sKey = String.fromCharCode(e.which);
		switch (e.keyCode) {
		case e.DOM_VK_TAB: return sDisplayKey + 'Tab';
		case e.DOM_VK_CLEAR: return sDisplayKey + 'Clear';
		case e.DOM_VK_RETURN: return sDisplayKey + 'Return';
		case e.DOM_VK_ENTER: return sDisplayKey + 'Enter';
		case e.DOM_VK_PAUSE: return sDisplayKey + 'Pause';
		case e.DOM_VK_ESCAPE: return sDisplayKey + 'Esc';
		case e.DOM_VK_SPACE: return sDisplayKey + 'Space';
		case e.DOM_VK_PAGE_UP: return sDisplayKey + 'PgUp';
		case e.DOM_VK_PAGE_DOWN: return sDisplayKey + 'PgDn';
		case e.DOM_VK_END: return sDisplayKey + 'End';
		case e.DOM_VK_HOME: return sDisplayKey + 'Home';
		case e.DOM_VK_LEFT: return sDisplayKey + 'Left Arrow';
		case e.DOM_VK_UP: return sDisplayKey + 'Up Arrow';
		case e.DOM_VK_RIGHT: return sDisplayKey + 'Right Arrow';
		case e.DOM_VK_DOWN: return sDisplayKey + 'Down Arrow';
		case e.DOM_VK_PRINTSCREEN: return sDisplayKey + 'PrtSc';
		case e.DOM_VK_INSERT: return sDisplayKey + 'Ins';
		case e.DOM_VK_DELETE: return sDisplayKey + 'Del';
		case e.DOM_VK_NUMPAD0: return sDisplayKey + 'NumPad 0';
		case e.DOM_VK_NUMPAD1: return sDisplayKey + 'NumPad 1';
		case e.DOM_VK_NUMPAD2: return sDisplayKey + 'NumPad 2';
		case e.DOM_VK_NUMPAD3: return sDisplayKey + 'NumPad 3';
		case e.DOM_VK_NUMPAD4: return sDisplayKey + 'NumPad 4';
		case e.DOM_VK_NUMPAD5: return sDisplayKey + 'NumPad 5';
		case e.DOM_VK_NUMPAD6: return sDisplayKey + 'NumPad 6';
		case e.DOM_VK_NUMPAD7: return sDisplayKey + 'NumPad 7';
		case e.DOM_VK_NUMPAD8: return sDisplayKey + 'NumPad 8';
		case e.DOM_VK_NUMPAD9: return sDisplayKey + 'NumPad 9';
		case e.DOM_VK_MULTIPLY: return sDisplayKey + 'NumPad *';
		case e.DOM_VK_ADD: return sDisplayKey + 'NumPad +';
		case e.DOM_VK_SEPARATOR: return sDisplayKey + 'NumPad Sep';
		case e.DOM_VK_SUBTRACT: return sDisplayKey + 'NumPad -';
		case e.DOM_VK_DECIMAL: return sDisplayKey + 'NumPad .';
		case e.DOM_VK_DIVIDE: return sDisplayKey + 'NumPad /';
		case e.DOM_VK_F1: return sDisplayKey + 'F1';
		case e.DOM_VK_F2: return sDisplayKey + 'F2';
		case e.DOM_VK_F3: return sDisplayKey + 'F3';
		case e.DOM_VK_F4: return sDisplayKey + 'F4';
		case e.DOM_VK_F5: return sDisplayKey + 'F5';
		case e.DOM_VK_F6: return sDisplayKey + 'F6';
		case e.DOM_VK_F7: return sDisplayKey + 'F7';
		case e.DOM_VK_F8: return sDisplayKey + 'F8';
		case e.DOM_VK_F9: return sDisplayKey + 'F9';
		case e.DOM_VK_F10: return sDisplayKey + 'F10';
		case e.DOM_VK_F11: return sDisplayKey + 'F11';
		case e.DOM_VK_F12: return sDisplayKey + 'F12';
		case e.DOM_VK_F13: return sDisplayKey + 'F13';

		case e.DOM_VK_F14: return sDisplayKey + 'F14';
		case e.DOM_VK_F15: return sDisplayKey + 'F15';
		case e.DOM_VK_F16: return sDisplayKey + 'F16';
		case e.DOM_VK_F17: return sDisplayKey + 'F17';
		case e.DOM_VK_F18: return sDisplayKey + 'F18';
		case e.DOM_VK_F19: return sDisplayKey + 'F19';
		case e.DOM_VK_F20: return sDisplayKey + 'F20';
		case e.DOM_VK_F21: return sDisplayKey + 'F21';
		case e.DOM_VK_F22: return sDisplayKey + 'F22';
		case e.DOM_VK_F23: return sDisplayKey + 'F23';
		case e.DOM_VK_F24: return sDisplayKey + 'F24';
		case e.DOM_VK_NUM_LOCK: return sDisplayKey + 'NumLk';
		case e.DOM_VK_SCROLL_LOCK: return sDisplayKey + 'ScrLk';
		}
		if (/^[a-zA-z0-9;=,\`\.\/;\'\[\]\\]$/.test(sKey)) {
		return sDisplayKey + sKey.toUpperCase();
		}
		return '';
	}
	
	function Magic LinemagicSubmit(event) {
		var elmForm = event ? event.target : this;
		while (elmForm.nodeName.toUpperCase() != 'FORM') {
		elmForm = elmForm.parentNode;
		}
		if (!elmForm.id || elmForm.id != 'magicform') {
		return elmForm._submit();
		}
		var doc = window.top.document;
		var elmMagicLine = doc.getElementById('magicline');
		var usMagicLine = elmMagicLine.value;
		usMagicLine = usMagicLine.normalize();
		var elmMagicResults = doc.getElementById('magicresults');
		if (elmMagicResults) {
		var sURL = gURLs[gSelectedIndex];
		var arKeywords = usMagicLine.split(' ');
		arKeywords.sort();
		usMagicLine = arKeywords.join(' ');
		var iHistoryIndex = findHistory(usMagicLine);
		GM_setValue('history.' + iHistoryIndex + '.keywords',
				usMagicLine);
		GM_setValue('history.' + iHistoryIndex + '.url', sURL);
		window.setTimeout(function() {
			window.top.location.href = sURL;
		}, 0);
		} else if (usMagicLine) {
		var ssMagicLine = escape(usMagicLine);
		window.setTimeout(function() {
			window.top.location.href = GM_getValue('searchengine',
				'http://www.google.com/search?q=') + ssMagicLine;
		}, 0);
		}
		magicHide();

		event.preventDefault();
		return false;
	}
	
	function Magic LinemagicKeypress(event) {
		var sKey = displayKeyFromEvent(event);
		if (sKey == 'Esc') {
		magicHide();
		return true;
		} else if (sKey == 'Up Arrow' || sKey == 'Down Arrow') {
		magicScrollResults(sKey == 'Up Arrow');
		event.preventDefault();
		return false;
		}
		window.setTimeout(function() {
		if (!magicSearch()) {
			magicHideResults();
		}
		}, 0);
		return true;
	}

	function magicHideResults() {
		var doc = window.top.document;
		var elmMagicResults = doc.getElementById('magicresults');
		if (!elmMagicResults) { return; }
		elmMagicResults.parentNode.style.height = '76px';
		elmMagicResults.parentNode.removeChild(elmMagicResults);
	}
	
	function magicScrollResults(bUp) {
		var doc = window.top.document;
		var elmMagicResults = doc.getElementById('magicresults');
		if (!elmMagicResults) { return; }
		var iNumResults = gURLs.length;
		if (iNumResults <= 1) { return; }
		var iOldSelectedIndex = gSelectedIndex;
		var iNewSelectedIndex = iOldSelectedIndex + (bUp ? -1 : 1);
		if (iNewSelectedIndex < 0) {
		iNewSelectedIndex = iNumResults - 1;
		}
		if (iNewSelectedIndex >= iNumResults) {
		iNewSelectedIndex = 0;
		}
		var arResults = elmMagicResults.getElementsByTagName('li');
		var elmOld = arResults[iOldSelectedIndex];
		var elmNew = arResults[iNewSelectedIndex];
		elmOld.style.backgroundColor = '#333';
		elmOld.style.color = 'white';
		elmNew.style.backgroundColor = '#ccc';
		elmNew.style.color = 'black';
		gSelectedIndex = iNewSelectedIndex;
	}

	function Magic LinemagicSearch() {
		var doc = window.top.document;
		var elmMagicLine = doc.getElementById('magicline');
		if (!elmMagicLine) { return false; }
		var sKeywords = elmMagicLine.value;
		sKeywords = sKeywords.toLowerCase().normalize();
		if (!sKeywords) { return false; }
		var arKeywords = sKeywords.split(' ');
		arKeywords.sort();
		sKeywords = arKeywords.join(' ');
		var iHistoryIndex = findHistory(sKeywords);
		var arResults = [];
		for (var i = 0; sMagicSearch = GM_getValue(i +
				   '.magicsearch'); i++) {
		var iMatch = -1;
		var iLowestMatch = 99999;
		for (var j = 0; j < arKeywords.length; j++) {
			iMatch = sMagicSearch.indexOf(arKeywords[j]);
			if (iMatch == -1) { break; }
			if (iMatch < iLowestMatch) {
			 iLowestMatch = iMatch;
			}
		}
		if (iMatch == -1 || iLowestMatch == 99999) { continue; }
		arResults.push(iLowestMatch.toString().lpad('0', 6) + ' ' +
				i + ' ' + sMagicSearch);
		}
		if (!arResults.length) { return false; }
		arResults.sort();
		var elmMagicDiv = doc.getElementById('magicdiv');
		if (!elmMagicDiv) { return false; } // should never happen
		var elmMagicResults = doc.getElementById('magicresults');
		if (!elmMagicResults) {
		elmMagicResults = doc.createElement('div');
		elmMagicResults.id = 'magicresults';
		elmMagicResults.setAttribute("style",
			"opacity: 1.0; background-color: #333; color: white; " +
				"overflow: hidden;");
		elmMagicDiv.appendChild(elmMagicResults);
		}
		var htmlResults = '<ul style="opacity: 1.0; list-style: none; ' +
		'margin: 0; padding: 0; text-align: left;">';
		var sHistoryURL = GM_getValue('history.' + iHistoryIndex + '.url');
		if (sHistoryURL) {
		for (var iSelectedIndex = 0;
			 iSelectedIndex < arResults.length;
			 iSelectedIndex++) {
		    if (arResults[iSelectedIndex].split(' ')[2] == sHistoryURL) {
			var sHistoryResults = arResults[iSelectedIndex];
			for (var j = iSelectedIndex; j > 0; j--) {
				arResults[j] = arResults[j - 1];
			}

			arResults[0] = sHistoryResults;
			break;
			}
		}
		}
		gSelectedIndex = 0;
		gURLs = [];
		var iMaxResults = GM_getValue('maxresults', 6);
		for (var i = 0; (i < iMaxResults) && (i < arResults.length); i++) {
		var Magic LinearLines = [];
		var iIndex = arResults[i].split(' ')[1];
		var sURL = GM_getValue(iIndex + '.url', '');
		gURLs.push(sURL);
		var sTitle = GM_getValue(iIndex + '.title', '');
		var sName = GM_getValue(iIndex + '.name', '');
		var sDescription = GM_getValue(iIndex + '.description', '');
		var sKeywords = GM_getValue(iIndex + '.keywords', '');
		var sReferrer = GM_getValue(iIndex + '.referrer', '');
		var sSource = GM_getValue(iIndex + '.source', '');
		if (sTitle) {
			arLines.push(sTitle);
		}
		if (sName && (!sTitle || sName.containsAny(arKeywords))) {
			arLines.push(sName);
		}
		if (sDescription.containsAny(arKeywords)) {
			arLines.push(sDescription);
		}
		if (sKeywords.containsAny(arKeywords)) {
			arLines.push('tags: ' + sKeywords);
		}
		if (sReferrer.containsAny(arKeywords)) {
			arLines.push('via: ' + sReferrer);
		}
		if (sSource.containsAny(arKeywords)) {
			arLines.push('source: ' + sSource);
		}
		arLines.push(sURL);
		for (var j = 0; j < arKeywords.length; j++) {
			var sKeyword = arKeywords[j];
			for (var k = 0; k < arLines.length; k++) {
			arLines[k] = arLines[k].replaceString(sKeyword,
					  '<b>$1</b>');
			}
		}
			arLines[arLines.length - 1] = '<span style="font-size: 9px;">' +
			arLines[arLines.length - 1] + '</span>';
		htmlResults += '<li style="opacity: 1.0; display: block; ' +
			'margin: 0; padding: 5px 10px 5px 10px; line-height: 140%; ' +
			'text-align: left; font-weight: normal; font-size: 12px; ' +
			'font-family: Optima, Verdana, sans-serif; font-variant: none;';
		if (i == gSelectedIndex) {
			htmlResults += ' background-color: #ccc; color: black;';

		} else {
			htmlResults += ' background-color: #333; color: white;';
		}
		if (i > 0) {
			htmlResults += ' margin-top: 5px; border-top: 1px dotted #888;';
		}
		htmlResults += '"><nobr>';
		htmlResults += arLines.join('</nobr><br><nobr>');
		htmlResults += '</nobr></li>';
		}
		htmlResults += '</ul>';
		elmMagicResults.innerHTML = htmlResults;
		var style = getComputedStyle(elmMagicResults, '');
		elmMagicDiv.style.height = 76 + parseInt(style.height) + 'px';
		return true;
	}

	function magicHide() {
		var doc = window.top.document;
		var elmWrapper = doc.getElementById('magicwrapper');
		if (!elmWrapper) { return; }
		doc.getElementById('magicline').blur(); // fixes weird focus issue
		elmWrapper.parentNode.removeChild(elmWrapper);
		HTMLFormElement.prototype.submit = HTMLFormElement.prototype._submit;
		HTMLFormElement.prototype._submit = null;
		if (_onkeypress) {
		var unsafeDocument = document.wrappedJSObject || document;
		unsafeDocument.onkeypress = _onkeypress;
		_onkeypress = null;
		}
	}

	function magicShow() {
		var doc = window.top.document;
		var elmMagic = doc.createElement('div');
		elmMagic.id = 'magicwrapper';
		var iWidth = window.top.innerWidth;
		var iHeight = window.top.innerHeight;
		elmMagic.setAttribute("style", "z-index: 99998; position: fixed; " +
			"top: 0; left: 0; width: " + iWidth + "px; height: " + iHeight +
			"px; background-color: white; color: black; opacity: 0.88;");
		elmMagic.innerHTML = '<div id="magicdiv" style="z-index: 99999; ' +
		'opacity: 1.0; position: fixed; top: 50px; left: ' +
		((iWidth / 2) - 250) + 'px; width: 500px; height: 76px; ' +
		'-moz-border-radius: 1em; background-color: #333; ' +
		'color: white; margin: 0; padding: 0">' +
		'<div style="opacity: 1.0; display: block; margin: 10px 0 0 0; ' +
		'padding: 0; text-align: center; font-size: 12px; ' +
		'font-family: Optima, Verdana, sans-serif; font-weight: normal;' +
		'font-variant: small-caps; letter-spacing: 0.1em;">' +
		'&mdash;Magic LineMagic Line &mdash;</div><form id="magicform" ' +
		'style="opacity: 1.0; position: relative; padding: 0; ' +
		'margin: 1em 0 0 0;"><input style="opacity: 1.0; ' +
			
		'background: #333; color: white; border: 1px solid white; ' +
		'display: block; width: 460px; margin: 10px auto 10px auto; ' +
		'padding: 1px 1em 1px 1em; -moz-border-radius: 1em; ' +
		'font-size: 11px; font-family: Optima, Verdana, sans-serif;"' +
		'type="text" name="Magic Linemagicline" id="magicline" value="" ' +
		'autocomplete="off"></form></div>';
		HTMLFormElement.prototype._submit = HTMLFormElement.prototype.submit;
		HTMLFormElement.prototype.submit = magicSubmit;
		doc.body.appendChild(elmMagic);
		var elmForm = doc.getElementById('magicform');
		elmForm.addEventListener('submit', magicSubmit, true);
		elmForm.addEventListener('keypress', magicKeypress, true);
		var unsafeDocument = document.wrappedJSObject || document;
		if (unsafeDocument.onkeypress) {
		_onkeypress = unsafeDocument.onkeypress;
		unsafeDocument.onkeypress = null;
		}
		doc.getElementById('magicline').focus();
	}
	
	function onkeypress(event) {
		var doc = window.top.document;
		var elmMagicWrapper = doc.getElementById('magicwrapper');
		if (elmMagicWrapper) { return true; }
		var sDisplayKey = displayKeyFromEvent(event);
		var sMagicKey = GM_getValue('key', 'Ctrl + Shift + L');
		if (!sDisplayKey || sDisplayKey != sMagicKey) {
		return true;
		}
		magicShow();
		event.preventDefault();
		return false;
	}

	document.addEventListener('keypress', onkeypress, true);
	if (/^http/.test(window.top.location.href)) {
		window.addEventListener('load', function() {
		window.setTimeout(function() {
			collectPageInfo();
			collectXFNInfo();
			collectFeedInfo();
			collectFOAFInfo();
		}, 0);
		}, true);
	}

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://www.philringnalda.com. The site looks the same as it always does. The script does not make any visible changes, but rest assured it is working hard under the covers. Press Ctrl-Shift-L, and you will see the Magic Line interface, as shown in Figure 12-13.

Figure 12-13. Magic Line interface

Magic Line interface

Start typing my name: Mark Pilgrim. Magic Line immediately knows my name and home page (http://diveintomark.org), because Phil lists me as one of his friends in his FOAF file. Without ever visiting my site, Magic Line has learned about me through Phil, as shown in Figure 12-14.

Figure 12-14. Magic Line autocompletion

Magic Line autocompletion

The power of this hack grows over time. Leave it running for a day; then come back tomorrow and try to find an article you read that you can't quite remember. Was it "Top 10 CSS Tricks"? Or "CSS Techniques You Need To Know"? Was it at evolt.org or alistapart.com, or some random weblog you'd never visited before? Magic Line will find it. Just bring it up and start typing.

Personal tools