Greasemonkey Hacks/Site Integration

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


Hacks 90–94: Introduction

One of the most powerful features of Greasemonkey scripts is the ability to integrate different sites in ways that neither site expected. This can be as simple as adding a form on one site that submits data to another site, or as complex as pulling data from disparate sites and combining them dynamically.

Most of the hacks in this chapter rely on a Greasemonkey API function called GM_xmlhttpRequest, which allows user scripts to get and post data to any site, anywhere, at any time. As you may recall from "Avoid Common Pitfalls" [Hack #12], this function was the center of a number of security holes in previous versions of Greasemonkey. Those vulnerabilities have long since been resolved, but you should always be aware of the power that Greasemonkey provides. It's a wonderful thing, but like every sufficiently advanced technology, it can be used for evil as well as good.

All the scripts in this chapter are safe to use, which is to say that they only do what they claim to do. Where there are unavoidable privacy concerns, I call them out specifically in the text.

Translate Any Web Page

Add a form at the top of every web page to translate it into your language.

Google Language Tools offers automated online translation of any web page. It's simple to use; just visit, enter the URL of the page, and select the source and target languages.

As is the case with so many web services, it would be even simpler to use if it were integrated with the web pages you visit. This hack adds a form at the top of every web page to hook it into Google's translation service.

The Code

This user script runs on all pages. It contains a hardcoded matrix of all the translations that Google Language Tools can perform automatically. English dominates the lists, as both a source and a target language. The script attempts to autodiscover the page's language by looking for a lang attribute on the <html> element. In XHTML, authors can also specify the language in the xml:lang attribute, but that functionality is left as an exercise for the reader. In theory, authors can also specify the language in the Content-Language HTTP header, but HTTP headers are not accessible to user scripts, so we can't check for that either.

On the bright side, the script does remember your previous choices for source and target languages, using the GM_setValue and GM_getValue functions to store your preferences in the local Firefox preferences registry.

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

		// ==UserScript==
		// @name		  Translate Page
		// @namespace
		// @description   translate pages with Google Language Tools
		// @include		  http://*
		// @include		  https://*
		// @exclude*
		// @exclude*
		// ==/UserScript==
		if (location.pathname == '/translate_c') return;
		var arArTranslate = {};
		arArTranslate['en'] = ['de', 'es', 'fr', 'it', 'pt', 'ja', 'ko', 'zh-CN'];
		arArTranslate['de'] = ['en', 'fr'];
		arArTranslate['es'] = ['en'];
		arArTranslate['fr'] = ['en', 'de'];
		arArTranslate['it'] = ['en'];
		arArTranslate['pt'] = ['en'];
		arArTranslate['ja'] = ['en'];
		arArTranslate['ko'] = ['en'];
		arArTranslate['zh-CN'] = ['en'];
		var arTranslateName = { 
			'en': 'English', 
			'es': 'Spanish', 
			'de': 'German', 
			'fr': 'French', 
			'it': 'Italian', 
			'pt': 'Portuguese', 
			'ja': 'Japanese', 
			'ko': 'Korean', 
			'zh-CN': 'Chinese (Simplified)'};

		var langSource; 
		var attrLang = document.evaluate("//html/@lang", document, null, 
			XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; 
		if (attrLang) {
			langSource = attrLang.value; }
		if (!(langSource in arArTranslate)) {
			langSource = GM_getValue('lang.source') || 'en'; 
		var langTarget = GM_getValue('') || arArTranslate[langSource][0];
		for (var i = arArTranslate[langSource].length; i >= 0; i--) {
			if (arArTranslate[langSource][i] == langTarget) break; 
		if (i < 0) {
			langTarget = arArTranslate[langSource][0]; 
		var elmTranslateDiv = document.createElement('div'); = '1px solid silver'; = 'right';
		var htmlSelect = '<select name="langpair" id="langpair">';
		for (var langOneSource in arArTranslate) {
			for (var i = 0; i < arArTranslate[langOneSource].length; i++) {
				langOneTarget = arArTranslate[langOneSource][i];
				htmlSelect += '<option value="' + langOneSource + '|' +
				langOneTarget + '"' +
				(((langOneSource == langSource) && (langOneTarget ==
		langTarget)) ?
				 ' selected' : '') + '>' + arTranslateName[langOneSource] + 
				 ' to ' + arTranslateName[langOneTarget] + '</option>';
		htmlSelect += '</select> ';
		elmTranslateDiv.innerHTML =
			 '<form id="translatepage" Googleweb page translation using Google Language Toolsmethod="GET" ' +
			 'action="" ' +
			 'style="font-size: small; font-family: sans-serif;">' +
			 'Translate this integrationpage top form for language translationpage from ' +
			 htmlSelect +
			 '<input type="hidden" name="u" value="' + location + '">' +
			 '<input type="hidden" name="hl" value="en">' +
			 '<input type="hidden" name="c2coff" value="1">' +
			 '<input type="hidden" name="ie" value="UTF-8">' +
			 '<input type="hidden" name="oe" value="UTF-8">' +
			 '<input type="submit" value="Translate">' +
		document.body.insertBefore(elmTranslateDiv, document.body.firstChild); 
		var elmTranslateForm = document.getElementById('translatepage');
		if (!elmTranslateForm) return;
		elmTranslateForm.addEventListener('submit', function(event) {
			 var elmSelect = document.getElementById('langpair');
			 if (!elmSelect) return true;
			 var ssValue = elmSelect.value;
	 var langSource = ssValue.substring(0, ssValue.indexOf('|'));
			 var langTarget = ssValue.substring(ssValue.indexOf('|') + 1);
			 GM_setValue('lang.source', langSource);
			 GM_setValue('', langTarget);
			 return true;
		}, true);

Running the Hack

After installing the user script (Tools → Install This User Script), go to At the top of the page, you will see drop-down box labeled "Translate this page from." This hack tries to autopopulate the source language by looking at the page's metadata. However, many pages do not properly specify their language, so you might need to tweak the value manually.

After selecting the appropriate values, click Translate to see Google's translation, as shown in Figure 11-1.

Figure 11-1. Greasemonkey home page in Spanish

Greasemonkey home page in Spanish

Since the translation is done entirely by a computer, it is far from perfect. In some cases, it is wildly and humorously inaccurate. (There are entire sites devoted to cataloging humorous computer translations of famous texts. Some people have entirely too much free time.) But Google's autotranslation will usually be accurate enough to give you a general overview of what the author was trying to express.

Warn Before Buying an Album

Find out whether an album is produced by a record label that supports the RIAA.

There are people in the world who dislike the Recording Industry Association of America (RIAA) because of their simultaneous disregard for both artists' rights (cheating artists with lopsided contracts) and customers' rights (suing fans and treating them like thieves). I am not one of those people, but I still like this hack, because it demonstrates Greasemonkey's role in enabling what I call passive activism.

My theory is that there is a small group of activists who will go out of their way to boycott the RIAA. But there is a much larger group of people who would like to boycott, but they don't actually get around to doing the necessary research when they're about to buy something. This hack helps that larger group, by adding an icon next to an album title on that shows whether this album is produced by a record label that supports the RIAA. It doesn't prevent you from buying the album; it just reminds you that you once cared enough to install a script that would remind you to think about this issue before buying.

The Code

This user script runs on all pages. It parses the URL to get the ASIN—a globally unique identifier that identifies the album you're browsing—and then uses the GM_xmlhttpRequest function to check the Magnetbox ( database to determine whether this album is produced by a company that supports the RIAA.


This script sends information about your browsing habits to Magnetbox. You should run this script only if you are comfortable exposing this information.

Save the following user script as riaa-radar.user.js:

		// ==UserScript==
		// @name		RIAA Radar
		// @namespace
		// @description Warn before buying albums produced by RIAA-supported labels
		// @include		http://*.amazon.tld/*
		// ==/UserScript==
		// based on code by Ben Tesch
		// included here with his gracious permission
		var radar = '';
		var asin = "";
		var index = location.href.indexOf('/-/');
		if (index != -1) {
			asin = location.href.substring(index + 3, index + 13);
		} else { 
			index = location.href.indexOf('ASIN'); 
			if (index != -1) {
				asin = location.href.substring(index + 5, index + 15);
		if (!asin) { return; }
		GM_xmlhttpRequest({method:'GET', url: radar + asin,
			onload:function(results) {
			var status = "unknown";

			if (results.responseText.match('button_warn.gif')) {
				status = "Warning!";
			} else {
				if (results.responseText.match('No album was found.')) {
				status = "Unknown";
				} else {
				status = "Safe!";

			var origTitle = document.evaluate("//b[@class='sans']",
				document, null, XPathResult.FIRST_ORDERED_NODE_TYPE,
			if (!origTitle) { return; }
			var div = origTitle.parentNode;
			var titlechld = origTitle.firstChild;
			var title = titlechld.nodeValue;
			var newTitle = document.createElement('b');
			newTitle.setAttribute('class', 'sans');
			var titleText = document.createTextNode(title);
			var sp = document.createTextNode(' ');
			var link = document.createElement('a');
			link.setAttribute('title', "RIAA Radar");
			link.setAttribute('href', radar + asin);
			var pic = document.createElement('img');
			pic.setAttribute('title', "RIAA Radar: " + status);
			if (status == 'Warning!') {
				pic.src = "";
			} else if (status == 'Safe!') {
				pic.src = "";
			} else {
				pic.src = "
			} = "0px";
				div.insertBefore(newTitle, origTitle);
				div.insertBefore(sp, origTitle);
				div.insertBefore(link, origTitle);

Running the Hack

After installing the user script (Tools → Install This User Script), go to and search for dave matthews stand up. Click through to the album page. At the top of the page, next to the album title, you will see a warning icon, as shown in Figure 11-2.

Figure 11-2. "Warning" icon next to album title

"Warning" icon next to album title

The warning icon indicates that this album was produced by a record label that supports the RIAA. The script does not prevent you from buying the album, it simply informs you of its status.

Now, search for another album—for example, astral projection another world. Click through to the album page and you will see a "safe" icon next to the album title, as shown in Figure 11-3.

This album is produced by an independent record label out of Israel that is not a member of the RIAA.

Figure 11-3. "Safe" icon next to album title

"Safe" icon next to album title

Find Out Who's Reading What You're Reading

Use Feedster to find weblogs that link to the current page.

Feedster ( is an RSS search engine that tracks tens of thousands of weblogs and news sites in almost real time. Not only is it a great way to find out what people are talking about, but it can also be used to discover what pages people are linking to.

But why limit yourself to searching manually to find out who's linking to a particular page? This hack adds a window that floats above web pages and shows you who is linking to the page and what they're saying about it.


Because this script gets information from a central source (Feedster), the operators of Feedster will be able to track the pages you visit. By default, this hack will not retrieve any information from Feedster until you click the triangle icon to expand the OmniFeedster floating window. You remain in control of when the script "phones home" to Feedster.

The Code

This user script runs on all http:// pages.


For privacy reasons, it will not run on https:// pages unless you explicitly change the default configuration.

The code is divided into three parts:

  1. The getFeedsterLinks function is the main entry point for retrieving information from Feedster. Feedster provides link information as an RSS feed. All you need to do is construct the appropriate URL and then parse the XML results. The getFeedsterLinks function retrieves the Feedster RSS feed with GM_xmlhttpRequest, parses it with Firefox's native DOMParser object, iterates through it to create an HTML representation, and then calls _refresh to update the OmniFeedster floating window.
  2. Several functions manage the OmniFeedster floating window. The mousedown and mouseup listeners call getDraggableFromEvent to allow you to move the floating window, and savePosition stores the position locally so it reappears in the same place when you follow a link or refresh the page.
  3. The createFloater function creates the floating window itself, adds it to the page, and positions it based on the saved coordinates.

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

		// ==UserScript==
		// @name		  OmniFeedster
		// @namespace
		// @description	  display who's linking to this page via Feedster
		// @include		  http://*
		// ==/UserScript==
		var _expanded = false;
		function getFeedsterLinks(sID) {
			var urlFeedster = '' +
				'type=rss&limit=5&url=' + escape(getCurrentUrl());
				method: 'GET',
				url: urlFeedster,
				onload: function(oResponseDetails) {
				if (oResponseDetails.status != 200) {
				_refresh(sID, '');
				var oParser = new DOMParser();
				var oDom = oParser.parseFromString(
				oResponseDetails.responseText, 'application/xml');
				if (oDom.firstChild.nodeName == 'parsererror') {
				_refresh(sID, '');
				var html, arItems, oItem, urlLink, sTitle, sDescription;
				html = '<ul style="list-style: none; margin: 0; padding: 0">';
				arItems = oDom.getElementsByTagName('item');
				for (var i = 0; i < arItems.length; i++) {
				oItem = arItems[i];
				urlLink = oItem.getElementsByTagName('link')[0].textContent;
				sTitle = oItem.getElementsByTagName('title')[0].textContent;
				sDescription = unescape(oItem.getElementsByTagName(
				html += '<li><a style="display: block; padding-bottom:2px; ' +
				'border-bottom: 1px solid #888; text-decoration: none; ' + 
				'background-color: transparent; color: navy; font: 10px ' +	
				'"Gill Sans", Verdana, sans-serif; font-weight: normal; ' +
				'font-variant: none;" href="' + urlLink + '" title="' + 
				sDescription + '">' + sTitle + '</a></li>';
				html += '</ul>';
				_refresh(sID, html);

				function _refresh(sID, htmlContent) {
				var elmFloater = document.getElementById(sID);
				if (!elmFloater) { return; } 
				var elmContent = document.getElementById(sID + '_content');
				if (!elmContent) { return; }
				elmContent.innerHTML = htmlContent +
				'[<a style="text-decoration: none; background-color: ' + 
				'transparent; color: navy; font: 10px "Gill Sans", Verdana, ' + 
				'sans-serif; font-weight: normal; font-variant: none;" ' +
				'title="Find this page on Feedster!" ' + 
				'href="' +
				escape(getCurrentUrl()) + '">more</a>]';
				var style = getComputedStyle(elmContent, '');
				var iHeight = parseInt(style.height) + 15;
				elmFloater.height = iHeight;
				GM_setValue(getPrefixFromID(sID) + '.height', iHeight);

				function getCurrentUrl() { 
				var urlThis = location.href;
				var iHashPos = urlThis.indexOf('#'); 
				if (iHashPos != -1) {
				urlThis = urlThis.substring(0, iHashPos);
				return urlThis; 

				function getDraggableFromEvent(event) {
				var elmDrag =;
				if (!elmDrag) { return null; } 
				while (elmDrag.nodeName != 'BODY' &&
				elmDrag.className != 'drag' &&
				elmDrag.className != 'nodrag') {
				elmDrag = elmDrag.parentNode;
				if (elmDrag.className != 'drag') { return null; }
				return elmDrag;

				document.addEventListener('mousedown', function(event) {
				var elmDrag = getDraggableFromEvent(event); 
				if (!elmDrag) { return true; } 
				var style = getComputedStyle(elmDrag, ''); 
				var iStartElmTop = parseInt(; 
				var iStartElmLeft = parseInt(style.left); 
				var iStartCursorX = event.clientX; 
				var iStartCursorY = event.clientY; 
				elmDrag._mousemove = function(event) { = (event.clientY + iStartElmTop -
				iStartCursorY) + 'px'; = (event.clientX + iStartElmLeft	-
				iStartCursorX) + 'px';
				return false;
				document.addEventListener('mousemove', elmDrag._mousemove, true);
				return false;
				}, true);

				document.addEventListener('mouseup', function(event) {
				var elmDrag = getDraggableFromEvent(event);
				if (!elmDrag) { return true; } 
				document.removeEventListener('mousemove', elmDrag._mousemove, true);
				}, true);
				function getPrefixFromID(sID) {
				return 'floater.' + sID;

				function savePosition(elmDrag) { 
				var sID =; 
				var style = getComputedStyle(elmDrag, '');
				GM_setValue(getPrefixFromID(sID) + '.left', parseInt(style.left));
				GM_setValue(getPrefixFromID(sID) + '.top', parseInt(; 

				function createFloater(sTitle, sID) {
				var elmFloater = document.createElement('div'); = sID; 
				elmFloater.className = 'drag'; 
				var iLeft = GM_getValue(getPrefixFromID(sID) + '.left', 10);
				var iTop = GM_getValue(getPrefixFromID(sID) + '.top', 10);
				var iWidth = GM_getValue(getPrefixFromID(sID) + '.width', 150);
				_integrationweblog and news site trackingexpanded = GM_getValue(getPrefixFromID(sID) +
				'.expanded', false);
				var iHeight = _expanded ? GM_getValue(
				getPrefixFromID(sID) + '.height', 100) : 13;
				elmFloater.setAttribute('style', 'position: absolute; left: ' +
				iLeft + 'px; top: ' + iTop + 'px; width: ' + iWidth + 
				'px; height: ' + iHeight + 'px; font: 9px Verdana, sans-serif; ' + 
				'background-color: #faebd7; color: #333; opacity: 0.9; ' + 
				'z-index: 99; border: 1px solid black');

				var elmHeader = document.createElement('h1'); = sID + '_header';
				elmHeader.setAttribute('style', 'position: relative; margin: 0; ' +
				'padding: 0; left: 0; top: 0; width: 100%; height: 13px; ' + 
				'background-color: navy; color: #eee; text-align: center; ' + 
				'opacity: 1.0; cursor: move; font: 9px Verdana, sans-serif;');
				if (sTitle) {

				var elmContent = document.createElement('div'); = sID + '_content';
				elmContent.className = 'nodrag';
				elmContent.setAttribute('style', 'position: absolute; top: 14px; ' +
				'left: 0; width: 100%; overflow: hidden; background-color: ' + 
				'#faebd7; color: #333; border: 0; margin: 0; padding: 0; ' + 
				'font: 10px "Gill Sans", Verdana, sans-serif'); = _expanded ? 'block' : 'none';
				elmContent.value = GM_getValue(getPrefixFromID(sID) + '.text', ''); 

				var elmExpand = document.createElement('a'); = sID + '_expand';
				elmExpand.className = 'nodrag';
				elmExpand.innerHTML = _expanded ? '&#9660;' : '&#9654;';
				elmExpand.setAttribute('style', 'display: block; position: ' +
				'absolute; top: 1px; left: 1px; width: 8px; height: 8px; ' + 
				'font: 10px Verdana, sans-serif; border: 0; margin-top: ' + 
				(_expanded ? '0px' : '-2px') + '; padding: 0; ' + 
				'background-color: transparent; color: white; ' +
				'text-decoration: none');
				integrationweblog and news site trackingelmExpand.title = 'Show/hide details';
				elmExpand.href = '#';
				elmExpand.addEventListener('click', function(event) {
				_expanded = !_expanded; 
				GM_setValue(getPrefixFromID(sID) + '.expanded',
				_expanded); = (_expanded ?
				GM_getValue(getPrefixFromID(sID) + '.height', 100) : 13) + 'px';
				elmExpand.innerHTML = _expanded ? '&#9660;' : '&#9654;'; = _expanded ? '0px' : '-2px'; = _expanded ? 'block' : 'none';
				if (_expanded) {
				}, true);
				window.addEventListener('load', function() {
				if (_expanded) {
				}, true);

				createFloater('OmniFeedster', 'omnifeedster');

Running the Hack

After installing the user script (Tools → Install This User Script), go to At the top of the browser window, you will see a small navy bar titled OmniFeedster, as shown in Figure 11-4.

Click the triangle icon in the OmniFeedster bar to expand the window and fetch a list of pages that link to and comment on the current page. If you hover your cursor over a link, it will display a short excerpt of the remote page, as shown in Figure 11-5.

The script takes the excerpt from the description element in the Feedster search feed. Since some sites include HTML in their descriptions, the script strips all HTML formatting. This occasionally leads to nonsensical text if the original site's HTML does not linearize well, but it is generally readable.

Figure 11-4. OmniFeedster floating window

OmniFeedster floating window

Figure 11-5. OmniFeedster links for

OmniFeedster links for

Add Wikipedia Links to Any Web Page

Turn the Web into the ultimate cross-referenced library.

Stefan Magdalinski of ( created a bit of a stir with his WikiProxy, which added links to the BBC's news articles that pointed to pages in the online encyclopedia Wikipedia ( The proxy worked by reading in a BBC page, extracting candidates for linking using specially tailored regular expressions, and then comparing these candidates to a list of phrases from the Wikipedia database.

This raises the possibility of extending this functionality beyond the BBC site. It's not feasible to proxy the entire Web (unless you're Google), but it sounds like a perfect task for a Greasemonkey script. One big problem: you need to check the term candidates against the Wikipedia database, which weighs in at a hefty 18 megabytes for the article titles alone.

Stefan, author of the original WikiProxy, has kindly agreed to make the Wikipedia term lookup accessible as a web service. This hack uses his web service to look up possible Wikipedia entries and adds links to the current page based on the keyword lookup.


This script contacts a central server on every page load, which presents a privacy risk.

The Code

This user script runs on all pages. It is quite complex, but it breaks down into five steps:

Define useful variables
The first section defines several variables, including various versions of the Wikipedia icons to label the new links, regular expressions to identify possible terms, and the URLs for the keyword lookup service and for Wikipedia itself.
Define convenience functions
The addWikiLinkStyle function adds new global style information to the page so that the Wikipedia links change appearance when the mouse moves over them. The getTerms function retrieves all the possible terms from the page that match a given regular expression.
Call the keyword lookup service
The main part of the script uses three separate regular expressions to extract candidates for linking. (The third expression is for acronyms.) It then calls the web service using GM_xmlhttpRequest. The keyword lookup service works with GET or POST; we use the POST method because the list of candidate terms might be too long to fit in the URL of a GET request.
Add hyperlinks to the text
The web service request is performed asynchronously, so nothing happens until the server returns some results. The GM_xmlhttpRequest calls our onload callback function, which parses the XML returned from the keyword lookup service to get the terms that match the Wikipedia database. We use the matching terms to construct a regular expression, and then we iterate over all the text nodes in the HTML page and wrap each matched term with a link to the corresponding Wikipedia page.
Provide undo capability
Finally, we add a menu item to the Firefox menu bar, using GM_ registerMenuCommand, which removes the Wikipedia links we just added.

To minimize the load on Stefan's keyword lookup service, we use an associative array, usedTerms, to keep track of which term candidates have been found on the page. This saves time and bandwidth by ensuring that each potential keyword is checked only once.

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

	// ==UserScript==
	// @name				  Wikiproxy: Greasemonkey Edition
	// @namespace
	// @description			  Adds web pagesWikipedia linksWikipedia links to key terms in webpages
	// @include				  http://*
	// @exclude*
	// @exclude*
	// @exclude				  http://*.wikipedia.tld/*
	// ==/UserScript==
	// based on code by Matthew Gertner, Valentin Laube, and others 
	// and included here with their gracious permission
	var iconcolor = 0; // 0 blue, 1 green, 2 red
	var icons = [ 
	var icons2 = [
	var bgprefix = "url("
	var bgsuffix = ") center right no-repeat";

	var requestUrl = "";
	var integrationWikipedia linkswikipediaUrlPrefix = "";
	var excludeAncestors = ["a", "script", "style", "input",
		"textarea", "select", "option"];

	var excludeXPath = "ancestor::*[";
	for (var tagNum=0; tagNum<excludeAncestors.length; tagNum++)
		excludeXPath += (tagNum == 0 ? "" : " or ") + "self::" +
	excludeXPath += "]";

	// Regular expression definitions from News Wikiproxy
	var capsword = "A|[A-Z][a-zA-Z'0-9]{1,}";
	var fillerwords = "a|of|and|in|on|under|the";
	var middlewordre = "(" + capsword + "|" + fillerwords + "|[A-Z]\.)[ \\t]*";
	var endwordre = "(" + capsword + ")[ \\t]*";
	var acronymre = "\\b([A-Z][A-Z0-9]{2,})\\b";

	// Match either "Two Endwords" or "Endword and Some Middle Words"
	var greedyproperre = "\\b(" + endwordre + "(" + middlewordre + ")*" +
		endwordre + ")\\b";
	// Match without filler words (so if you have a phrase like
	// "Amnesty International and Human Rights Watch" you also get both parts 
	// separately "Amnesty International" and "Human Rights Watch")
	var frugalproperre = "\\b((" + endwordre + "){2,})\\b";

	var usedTerms = new Object();
	function integrationWikipedia linksaddWikiLinkStyle() {
		var wikiLinkStyle = document.createElement('style'); = "wikilinkstyle";
		wikiLinkStyle.type = "text/css";
		wikiLinkStyle.innerHTML = '.wikilink, .wikilink_over {\n'
			+ 'color: inherit;\n'
			+ 'Wikipediaadding links to web pagespadding-right: 13px;\n'
			+ '}\n'
			+ '.wikilink {\n'
			+ 'background: transparent ' + bgprefix
			+ icons[iconcolor] + bgsuffix + ';\n'
			+ '}\n'
			+ '.wikilink_over {\n'
			+ 'background: transparent ' + bgprefix
			+ icons2[iconcolor] + bgsuffix + ';\n'
			+ '}';

	function getTerms(str, regexpstr, terms) {
		var candidates = str.match(new RegExp(regexpstr, "mg")); 
		for (var i=0; i<candidates.length; i++) {
			var term = candidates[i];
			while (term.charAt(term.length-1) == " ")
				term = term.substring(0, term.length-1);
			if (usedTerms[term] == null) {
				if (terms.length > 0) {
				terms += " ";
				terms += term.replace(/ /g, "_");
				usedTerms[term] = term;
		return terms;

	if (document.documentElement.tagName == "HTML") {
		var treeWalker = document.createTreeWalker(
			document.documentElement, NodeFilter.SHOW_TEXT, null, false);
		var text = "";
		var textNode;
		while (textNode = treeWalker.nextNode()) {
			if (!document.evaluate("ancestor::script", textNode, null,
				XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue) {
				text += textNode.nodeValue + "\n";

		var terms = getTerms(text, greedyproperre, "");
		terms = getTerms(text, frugalproperre, terms);
		terms = getTerms(text, acronymre, terms);
			method: 'POST',
			url: requestUrl,
			headers: {
				'User-agent': 'Mozilla/4.0 (compatible) Greasemonkey',
				'Content-type': 'application/x-www-form-urlencoded'
			data: 'text=' + escape(terms),
			onload: function(responseDetails) {
				var parser = new DOMParser();
				var responseXML = parser.parseFromString(
				responseDetails.responseText, "text/xml");
				var termSnapshot = document.evaluate("/wikiproxy/term/text()",
				responseXML, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
				var normalizedTerms = new Object();
				var termRegExp = "";
				for (var i=0; i<termSnapshot.snapshotLength; i++)
				var termNodeValue = termSnapshot.snapshotItem(i).
				nodeValue.replace(/_/g, " ");
				normalizedTerms[termNodeValue.toLowerCase()] =
				if (termRegExp.length > 0) {
				termRegExp += "|";
				termRegExp += termNodeValue;
				termRegExp = new RegExp("\\b(" + termRegExp + ")\\b", "mg");
				treeWalker = document.createTreeWalker(
				document.documentElement, NodeFilter.SHOW_TEXT, null,
				while (textNode = treeWalker.nextNode())
				if (responseXML.evaluate(excludeXPath, textNode, null,
				null).singleNodeValue) { continue; }

				var matches = textNode.nodeValue.match(termRegExp);
				if (!matches) { continue; }
				// add wiki link style 
				if (!document.getElementById ('wikilinkstyle')) {
integrationWikipedia links
				           Wikipediaadding links to web pagesaddWikiLinkStyle(); 

				for (i=0; i<matches.length; i++)
				var term = matches[i];
				var displayTerm = term.replace(/_/g, " ");
				term = normalizedTerms[term.toLowerCase()];
				var termIndex = textNode.nodeValue.indexOf(displayTerm);
				var preTermNode = document.createTextNode(
				textNode.nodeValue.substring(0, termIndex));
				textNode.nodeValue = textNode.nodeValue.substring(
				var anchor = document.createElement("a");
				anchor.className = "wikilink";
				anchor.addEventListener('mousemove', function () {
				this.className = 'wikilink_over';
				}, true);
				anchor.addEventListener('mouseout', function () {
				this.className = 'wikilink';
				}, true);
				anchor.href = integrationWikipedia linkswikipediaUrlPrefix + term;
				var termNode = document.createTextNode(displayTerm);
				anchor.insertBefore(termNode, anchor.firstChild);
				textNode.parentNode.insertBefore(preTermNode, textNode);
				textNode.parentNode.insertBefore(anchor, textNode);

	function undoWikify() {
		var Wikipediaadding links to web pageswlinks = document.evaluate('//a[@class="wikilink"]',
			document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
		for (var i = 0; i < wlinks.snapshotLength; i++) {
			var wlink = wlinks.snapshotItem(i);
			var text = document.createTextNode(wlink.textContent);
			wlink.parentNode.replaceChild(text, wlink);
	GM_registerMenuCommand('Undo Wikify', undoWikify);

Running the Hack

After installing the script (Tools → Install This User Script), load The script converts all words on the page that have Wikipedia entries into links, decorated with a Wikipedia icon, as shown in Figure 11-6.

We take care not to change text that was already linked in the original page.

Matthew Gertner

Figure 11-6. Wikified CIA Factbook

Wikified CIA Factbook

Compare Book Prices

Add competitors' prices to online book retailers.

When the Web was new, pundits and trade magazines hyped the possibility of agents that followed you around online and fetched the lowest prices, or recommended similar sites, or made your coffee, or something. The hype died down quickly, but many of the best ideas have eventually resurfaced in one form or another. It turns out that most people don't actually care about some bureaucrat's idea of "similar sites," but everybody cares about finding the lowest prices.

This hack adds a floating window to online bookstores, such as Amazon. com, that shows you the price for the same book on other sites. Really.

The Code

This user script runs on several online book stores:


The script parses the ISBN—a globally unique identifier for the book you're viewing—and uses it to fetch pricing information from other sites. Sites like provide an official web services API for getting this information; for other sites, the script relies on tried-and-true techniques of screen scraping.


By default, this script adds associate IDs to the links it constructs, which gives the script's author a small referral fee. If you dislike this, or you have your own associate ID, you can change the first few lines of code to define your own IDs or remove them altogether.

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

	// ==UserScript==
	// @name		Book Burro - Remixing the bookstore
	// @namespace
	// @description Find the cheapest books
	// @include*
	// @include*
	// @include*
	// @include*
	// @include*
	// @include*
	// @include*
	// @include*
	// @include*
	// @exclude
	// ==/UserScript==
	// based on code by Jesse Andrews and Britt Selvitelle 
	// and included here with their gracious permission
	// Change these as desired
	var amazon_associate_code = 'anotherjesse-20';
	var amazon_dev_key = '0XYJJ825QSB9Q7F2XN02';
	var bn_associate_code = '41456445';
	var half_associate_code = '1698206-1932276';

	function checkISBN( isbn ) {
		try {       
			isbn=isbn.toLowerCase().replace(/-/g,'').replace(/ /g,'');
			if (isbn.length != 10) return false;
			var checksum = 0;
			for (var i=0; i<9; i++) {
				if (isbn[i] == 'x') {
				checksum += 10 * (i+1);
				} else {
				checksum += isbn[i] * (i+1);
			checksum = checksum % 11;
			if (checksum == 10) checksum = 'x';
			if (isbn[9] == checksum)
				return isbn;
				return false;
		} catch (e) { return false; } 

	function dom_createLink(url, txt, title) { 
		var a = document.createElement("a");
		a.setAttribute("href", url);
		a.setAttribute("style", "color: #00a; text-decoration: none; " +
			"font-weight: bold");
		if (title) a.setAttribute("title", title);
		return a;

	function add_site(url, title, loc_id ) {
		var a = dom_createLink( url, title, title + ' Search');
		var b = document.createElement("b");
		b.innerHTML = 'fetching';
		b.setAttribute("id", loc_id);

		var tr = document.createElement("tr");
		var td_left = document.createElement("td");
		var td_right = document.createElement("td");
		td_right.setAttribute("align", "right");
		return tr;

	function str2xml(strXML) { 
		//create a DOMParser
		var objDOMParser = new DOMParser();
		//create new document from string
		var objDoc = objDOMParser.parseFromString(strXML, "text/xml");
		return objDoc;

	function int2money( cents ) { 
		var money = "$"
			if (cents< 100) { 
		money = money + '0.';
			} else { 
				money = money + Math.floor(cents/100) + '.';
		cents = cents % 100;
		if (cents < 10)
			money = money + '0';
		money = money + cents;
		return money;

	function run_queries(isbn) {
		var errmsg = 'Either there are no book pricesbooks available,\\' +
			'or there is a parsing error because of\\n' +
			'some change to their website.\\n\\n' +
			'Not everyone has a nice webservice like Amazon';

		////// AJAX for /////

			url:'' +
			onload:function(result) {
				try {
				document.getElementById('burro_bn').innerHTML = 
				} catch (e) {
				document.getElementById('burro_bn').parentNode.innerHTML = 
				'<a href="javascript: alert(\''+errmsg+'\');">none</a>';

		////// AJAX for /////

				isbn, data:"",
			onload:function(result) {
				try { 
				document.getElementById('burro_buy').innerHTML =
				} catch (e) { 
				document.getElementById('burro_buy').parentNode.innerHTML =
				'<a href="javascript: alert(\''+errmsg+'\');">none</a>';

		////// AJAX for /////
			url:'' +
				'product=book pricesbooks:isbn&query='+isbn, data:"",
			onload:function(result) {
				try { 
				document.getElementById('burro_half').innerHTML = 
				} catch (e) { 
				document.getElementById('burro_half').parentNode.innerHTML = 
				'<a href="javascript: alert(\''+errmsg+'\');">none</a>';

			////// AJAX for /////
				 url:'' + amazon_associate_code + 
				 '&dev-t=' + amazon_dev_key +
				 '&type=lite&f=xml&mode=books&AsinSearch='+isbn, data:"",
				 onload:function(result) {
				 var x = str2xml( result.responseText );
				 var ourprices = x.getElementsByTagName('OurPrice'); 
				 if (ourprices.length == 0) {
			innerHTML = 
				'<a href="javascript: alert(\''+errmsg+'\');">none</a>
				 } else {

				document.getElementById('burro_amazon').innerHTML =
				book pricesourprices[0].childNodes[0].nodeValue;
				 var usedprices = x.getElementsByTagName('UsedPrice');
				 if (usedprices.length == 0) {
				var elmMarket = document.getElementById('burro_ 
				elmMarket.parentNode.innerHTML = 
				'<a href="javascript: alert(\''+errmsg+'\');">none</a>
				} else {
				document.getElementById('burro_amazonmarket').innerHTML =
			var msg = 'We want to check with them regarding the traffic of querying
				'for prices from their site on every click…';
			document.getElementById('burro_powell').parentNode.innerHTML =
				'<a href="javascript: alert(\''+msg+'\');">(info)</a>';

		function burro( location, isbn ) {
			var elmWrapper = document.createElement("div");
			elmWrapper.setAttribute("title","Click triangle to expand/collapse");
			elmWrapper.setAttribute("style",'position:fixed;z-index:99;top:15px;' +
				'left:15px;background-color:#ffc;border:1px solid orange;' +
				'padding:4px;text-align:left;opacity:.85;font:8pt sans-serif;' +
			var elmCaret = document.createElement("img");
			elmCaret.setAttribute("style", "top:-10px");
			elmCaret.setAttribute("src", 'data:image/png;base64,iVBORw0KGgoAAA' +
				'Q29tbWVudABDcmVhdGVkIHdpdGggVGhlIEdJTVDvZCVuAAAAiklEQVQY07X' +
				'PIQoCURSF4e8NAzYxGicNuAa1WlyCO3AlZnfiNgwahQFxikkcBIsGfaZpzg' +
				'ODJ/4c/nMvPyR8g7EsephgH6q6aXnWIelhjkUsi0EL88TqFUfMYlnscMoS5' + 
				'wUccMYS4yxhfuGNPho88oQ5xxQjrHHpKkcMccMqVPU99eATG2zb4n/zAS4O' +
			elmCaret.setAttribute("id", "hide_show_elmCaret");
			elmWrapper.appendChild( elmCaret );
			var elmTitle = document.createElement("img");
			elmTitle.setAttribute("style", "padding-left:6px");
			elmTitle.setAttribute("src", 'data:image/gif;base64,R0lGODlheAAO' +
				'AOYAAAAAAOmUUUOGfx1eVj8/M1ijnqBhLrmFVv//zNi6oCUIAO/vwJqEbGk' +
				'4EL+/mm9vWZmZmVpaWvfAl7ydiC8vJo+PcwAPCaJtSylNS////4tCC9/fs0' +
				'MeAa+vjefy9LR1QpnMzH9/Zny3s7WIazttbG5MMVyVj09PQJ+fgMvO11UwE' +
				'wMWGq10R5K6ugg2Nh8fGdiecOaxhdiKTAAACZlqPnWtqu/dxTxlYXNeS1o6' + 
				'IU8rEZqytlp2d4p4bDZ/dn+Eh4pWL2JNOFKLhf/frtusjtGFS750N8ulfo1' + 
				'qVM/Ppm2HhsSQa615UvSlYy5HS9rb1hAEDqdvPjgpJg8PDP/vx4S2s6toMz' +
				'RERKmlmrS8vkFfXwAhHnI7Ek06K+3x8D92c6/M0DNmZk5nb4xNGr+8s9uNT' +
				'8x9OysRAtSniJWOlq6GYwAICZGtsP+4dGVCLkaVj4GpqF9fTMuLW8q/ra1r' +
				'AAAB4AA4AAAf/gAiCg4MzM4SIgyo5hGcKj1JTiZOUlZaXmJmagmtrPEprlR' +
				'N9TA0ICjpIaiMjSEhdm7Gys7SInXd3JiQ3oZN8Bx9jXGpoaEtLFzRumkkEK' +
				'LTNz4nRtbWGbHBVJttCJBaTDQYaYzASSzAHQCxEmw4AIbTu8Iny1bMzWTtw' +
				'cH4Fbz4DBHhLZGfMmCJy5NCwY0qWuwdxCHQYVIHAiYmCkkR8sABBvQ4hkgx' +
				'6GBFjCGknPYYkUCGEgwcnNix4QCCOyEsrVoBpUUOEvwIAw4T5RkjFmA8faP' +
				'DhU8sdgAcUADhA8ODpCQATN0yhEGKrx3coAMQh5BSqVAQACAhKiyAEgCnv/' +
				'94CAICAwpQQdjdcmiFmRQuf/nwICDMAA4ZeCLrouBClMR8VJUrMcjd2A4CY' +
				'lwVNeYEgDgCRFQCgcHdVLVmxCCyfQGuardvVaD8jCFsBQRLUllY40eICRA0' +
				'BwL8I7ZRhUA8VNAx8sIOGSoISy2LVYz2dAF3rguQ5xT3y3Vq1bFm3PSt+/N' +
				'0hylV60uUNBVedl99SBmYXnGnnhhfaMapmsgYEY7bWwxRoZZJBCGlAMMgEQ' +
				'dMhghR5DxKAADWokYMMTWeyBiTtTdODZM3Z1EBo8HYjlgL9dC8izwBSmWRj' +
				'6NFEAKdawUIUTBBhgxfI+mjrssxiMpclXdhBRxlmmJEUDTQckcAcXnjwRKP' +
			elmWrapper.appendChild( elmTitle );
			var elmCloseBox = document.createElement("img");
			elmCloseBox.setAttribute("src", 'data:image/png;base64,iVBORw0KG' +
				'JREFUKM+Fkr1qAkEURs9dnWBrJT5IHkEw4gtEsBQs/AlpkiKlJGnEJlqIjR' +
				'brPoAQYhPio1hGsPAHFoW5KSIxo7J7qvlgDty534j6Rolgt987OQnA7XuEs' +
				'TuegwIeMYiIkx2hVnsjCL7+su9/0mz2Lox0oNOpUiw+kc2mUVWGww8mkxYi' +
				'YK09F4xJMho9kMs9IiJMpy8Y83vFWkUTCVcAWCxWLJcrRIT1OiSTOczuCXi' +
				'eK2y3IeXyK4PBPZtNSKn0zGzWJpW6uvyGer1LpVIgn78GYD7/ptHo0e/fHb' +
			elmCloseBox.setAttribute("style", 'position:absolute;left:190px;' +
				'top:3px;margin:2px;width:12px;height:12px;background-color:' +
			elmCloseBox.setAttribute("title","Click To Remove");
			elmCloseBox.addEventListener('click', function() { = "none";
			}, true);
			var elmAbout = document.createElement("a");
			var elmAboutImg = document.createElement("img");
			elmAboutImg.setAttribute("border", "0");
			elmAboutImg.setAttribute("src", 'data:image/png;base64,iVBORw0KG' +
				'JREFUKM+FkrFOAkEURc9ddrJZCgsqCwtCIiUFJa2df2CwNXRS8AmGisLEaG' +
				'Gs3VhTYDUtxZaED/AriGHjPhuEDC5wq3l5czL3vjeyzBlHtC6KoI4BuPk8Q' +
				'qx3549rov3+YPBIp3NHq3VLlnkkBf0AWK2+6fevWCze8H7CaPRKUfzgnKsG' +
				'0jSh272kLEuWyy/a7QvSNKnIsJEk6vWE2SxnPH5nOn3YWopjh9VqIbDBGA5' +
				'fyPNnGo2znZVIEEWhJTNDgmbzfHvZzA6H/pP3k3/TqQQkIYle7z6oT74wnz' +
			elmAbout.setAttribute("style", 'position:absolute;left:175px;top' +
				':3px;margin:2px;width:12px;height:12px;background-color:#ff' +
				'b;border:none;line-height:12px;text-align:center;text-decor' +
			elmAbout.setAttribute("href", '' +
				'/2005/04/24/greasemonkey-book pricesbook-burro-find-cheap-books');
			var elmContent = document.createElement("table");
			elmContent.setAttribute("style", 'padding:0 5px;width:100%;font:' +
				'10pt sans-serif;');
			elmContent.appendChild( add_site('' +
				'dos/ASIN/' + isbn + "/" + amazon_associate_code, "Amazon",
				"burro_amazon" ));
			elmContent.appendChild( add_site("" +
				"Amazon (used)", "burro_amazonmarket" ));
			elmContent.appendChild( add_site( '' + 
				't/click?bfmid=2181&sourceid=' + bn_associate_code +'&bfpid=' +
				isbn + '&bfmtype=book', "Barnes & Noble", "burro_bn"));
			elmContent.appendChild( add_site("" +
				"alSearchAction.asp?qu=" + isbn, "", "burro_buy"));
			elmContent.appendChild( add_site( '' +
				half_associate_code+'?ISBN=' + isbn, '', 'burro_half' ));
			elmContent.appendChild( add_site('' +
				'/biblio?isbn=' + isbn, "Powell's Books", "burro_powell"));
			elmWrapper.addEventListener('click', function() {
				var elmCaret = document.getElementById('hide_show_elmCaret');
				if ( != "auto") { 
				if ( == "15px") {
				run_queries( isbn ); 
				} = "auto";
				elmCaret["src"] = 'data:image/png;base64,iVBORw0KGgoAAAA' +
				'/kCuwAAAB10RVh0Q29tbWVudABDcmVhdGVkIHdpdGggVGhlIEdJ' +
				'Q29zCM3iSXEXQTrCws44Im85CkO1jnq2KKQWnGxiY+Yd/oXw1tT' +
				'rr7D86CYdD5Xk3HA8vTi8la7xW1T5Jg8IEN6PvPXnEAkOa5l5Vi' + 
				'wuc4yk/d9VyfoLrqnpIMmCGu2z79/wGUsv5FFcYY5Nt/wKjI+Bv' +
				} else {
				elmCaret["src"] = 'data:image/png;base64,iVBORw0KGgoAAAA' +
				'Q08hgAAAB10RVh0Q29tbWVudABDcmVhdGVkIHdpdGggVGhlIEdJ' +
				'CO3AlZnfiNgwahQFxikkcBIsGfaZpzgODJ/4c/nMvPyR8g7Esep' +
				'hgH6q6aXnWIelhjkUsi0EL88TqFUfMYlnscMoS5wUccMYS4yxhf' +
				'uGNPho88oQ5xxQjrHHpKkcMccMqVPU99eATG2zb4n/zAS4OHrV1' +
				'hIB/AAAAAElFTkSuQmCC'; = "14px";
			}, true);
		if (document.location.href.match('') &&
			!document.location.href.match('rate-this')) {
			isbn = checkISBN(
			if (isbn) burro( 'amazon', isbn ); 
		if (document.location.href.match('')) {
			isbn = checkISBN( document.location.href.match(
				/[iI][sS][Bb][Nn]=([0-9X]{10})(\&|\?|$)/)[1] );
			if (isbn) burro( 'bn', isbn );
		if (document.location.href.match('')) {
			var isbn = checkISBN(
				document.title.match(/ISBN ([0-9X]{10})/)[1] );
			if (isbn) burro( 'buy', isbn );
		if (document.location.href.match('')) {
			var arBold = document.getElementsByTagName('b');
			for (var i=0; i<arBold.length; i++) {
				if (arBold[i].innerHTML.match('ISBN:')) {
				isbn = checkISBN(arBold[i].nextSibling.nextSibling.text);
				if (isbn) burro( 'powells', isbn );

		if (document.location.href.match('')) {
			var arBold = document.getElementsByTagName('b');
			for	(var i=0; i<arBold.length; i++) {
				if (arBold[i].innerHTML.match('ISBN:')) { 
				isbn = checkISBN(arBold[i].nextSibling.text);
				if (isbn) burro( 'half', isbn );

Running the Hack

After installing this script (Tools → Install This User Script), go to and search for Harry Potter Half-Blood Prince. Click through to the book page. In the top-left corner is a small floating window titled Book Burro. Click the triangle to expand the window, and Book Burro will fetch prices from other online retailers, as shown in Figure 11-7.

Figure 11-7. Comparison shopping for Harry Potter

Comparison shopping for Harry Potter

Each of the competitors is a link to buy that book on another site. Click Barnes & Noble to go to the book page at, as shown in Figure 11-8.

Figure 11-8. Harry Potter on

Harry Potter on

The script will not fetch prices from other sites unless you expand the Book Burro window. You will need to expand the pricing window manually each time you go to a book page to see competitors' prices.

Personal tools