Greasemonkey Hacks/Search

From WikiContent

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


Contents

Hacks 47–59: Introduction

Search: next to shopping, emailing, and downloading pornography, it's the most popular activity on the Web. With billions of pages in no particular order, search engines have risen to prominence as the gatekeepers of the Internet. Those that survived the dot-com boom and bust are now worth hundreds of billions of dollars, and they probably deserve it. (They certainly deserve it more than their dot-com predecessors. Remember Pets.com? Man, that was a great business model. "Hey, let's sell 50-pound bags of dog food with free shipping." Brilliant.)

Google is currently the undisputed king of search, so many of the hacks in this chapter focus on Google. But there are hacks here for Yahoo! users too, and a few that work with any search engine.

Add a Site Search

Google can restrict your search to a specific site. You can take advantage of this feature to add a site search to every page you visit.

There are two ways to restrict Google to return pages on a specific site. The first way is to use the site: keyword in your search results, like this:

	foo site:example.com

This Google search searches for foo but returns only pages on the example.com domain. On the search results page, the URL looks like this:

	http://www.google.com/search?hl=en&q=foo+site%3Aexample.com 
         

The second way is to do it in two steps. First, search for foo; then, at the bottom of the search results, click "Search within results." You will get to a page with another search form, where you can enter site:example.com. The actual search results will be the same as the previous one-step method, but the URL looks different:

	http://www.google.com/search?hl=en&lr=&c2coff=1&q=foo&as_q=site%3Aexample.com
         

This difference is important, because the keyword foo and the site name site:example.com are in separate query parameters. It makes it trivial to reverse-engineer that URL to construct a form that searches a specific site. The form would display a single visible text box named q and also contain a hidden form field named as_q that contains the domain of the current page with a site: prefix.

The Code

The code is in two parts. The first part creates the site search form and inserts it at the top of the page. The second part styles the form so it is unobtrusive and visually separated from the rest of the page.

This script should run on all pages except pages on google.com. (It would be silly to include a site search on the search results page!)

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

	// ==UserScript==
	// @name			Site Search
	// @namespace		http://diveintomark.org/projects/greasemonkey/
	// @description		adds a site search on every page using Google Site Search
	// @include			http://*
	// @exclude			http://*.google.tld/*
	// ==/UserScript==
	
	var elmSearchDiv = document.createElement('div');
	elmSearchDiv.innerHTML =
		'<form method="GET" action="http://www.google.com/search">' +
		'<label for="as_q">Search this site:</label> ' + 
		'<input type="text" id="as_q" name="as_q" accesskey="S"> ' + 
		'<input type="hidden" name="q" value="site:' + location.host + '">' +
		'<input type="submit" value="Search">' +
		'</form>';
	document.body.insertBefore(elmSearchDiv, document.body.firstChild);
	elmSearchDiv.style.fontSize = 'small';
	elmSearchDiv.style.textAlign = 'right';
	elmSearchDiv.style.borderBottom = '1px solid silver';

Running the Hack

After installing the user script from Tools → Install This User Script, go to http://www.gnu.org. You should see a new form in the top-right corner of the page labeled "Search this site:", as shown in Figure 6-1.

Figure 6-1. Site search on www.gnu.org

Site search on www.gnu.org

Enter gpl compatible and click Search. You will be taken to the Google search results showing pages on www.gnu.org that reference GPL compatibility, as shown in Figure 6-2.

Figure 6-2. Site search results for "gpl compatible" on www.gnu.org

Site search results for "gpl compatible" on www.gnu.org

Hacking the Hack

Most search engines include functionality to restrict a search to a particular site. If you prefer to use a different search engine, just look at the URLs it uses to do site-specific searches and work your way back to construct the site search form to match.

For example, the relevant query string parameters of a site-specific search on Yahoo! Web Search look like this:

	http://search.yahoo.com/search?va=gpl+compatible&vs=www.gnu.org
            

The search keywords are in the va parameter, and the domain is in the vs parameter.

Tip

There's one difference from Google's site search: the domain to search is specified by itself, without a site: prefix.

To add a site search that uses Yahoo! Web Search, construct the form like so:

	elmSearchDiv.innerHTML =
		'<form method="GET" action="http://search.yahoo.com/search">' +
		'<label for="va">Search this site:</label> ' + 
		'<input type="text" id="va" name="va" accesskey="S"> ' + 
		'<input type="hidden" name="vs" value="' + location.host + '">' +
		'<input type="submit" value="Search">' +
		'</form>';

The rest of the script will work unchanged.

Tip

The innerHTML property is a great way to inject a complex chunk of HTML into a page. It is not part of the W3C DOM standard, but all modern browsers support it.

If you want the site search box to appear at the bottom of each page, instead of the top, change this line:

	document.body.insertBefore(elmSearchDiv, document.body.firstChild);

to this:

	document.body.appendChild(elmSearchDiv);

You can also alter the styling of the site search form itself. If you want to distinguish it visually from the rest of the page, you could give it a black background with white text. Add these two lines to the end of the user script:

	elmSearchDiv.style.backgroundColor = 'black';
	elmSearchDiv.style.color = 'white';

Remove Spammy Domains from Search Results

Fight back against search engine spammers who register domains with multiple "hot" keywords separated by hyphens.

Google and other search engines are engaged in an ongoing arms race against spammers, who use every conceivable trick to attain top placement for lucrative search keywords. One such trick is to register a domain name with the keywords themselves, such as buy-cheap-prescription-drugs-online.com. (I just made that up, although I Wouldn't be the slightest bit surprised if it already existed. In fact, I would be surprised if it didn't.) Recently, Google has cracked down on such techniques, but some spammy domains still show up in search results.

Think of the web sites you visit on a regular basis. I'll bet that none of them contains more than one hyphen. In fact, the only time I ever see multi-hyphen domain names is when a spammer is one step ahead of Google and manages to get his site listed in the results. (I don't buy cheap prescription drugs online, but I did need to refinance my home last year. Search engine results were so overwhelmed with spam, I almost broke down and used a phone book.)

The Code

This user script removes Google search results where the domain contains more than one hyphen. Once again, the bulk of the logic is contained in the XPath query. This is tricky for two reasons. First, we need to count the number of instances of a particular character in a string, and XPath doesn't have a native function to do that. Second, we need to isolate the entire search result—link, description, everything—and remove it all at once.

We can solve the first problem (counting the hyphens in the domain) by a clever use of the XPath translate function, which "translates" a string by replacing specific characters with other characters. The key here is to tell the translate function to replace a character with nothing (in other words, to remove it altogether). If we munge the URL in a certain way, and the result starts the string "//--", the original URL must have contained at least two hyphens in its domain. (Many legitimate web publishing systems generate URLs with multiple hyphens in the pathname, so we must be careful not match URLs such as http://diveintomark.org/archives/2004/08/13/safari-content-sniffing.)

Tip

A complete list of XPath functions is available at http://www.w3schools.com/xpath/xpath_functions.asp.

We can solve the second problem by using the ancestor:: axis. Each search result is wrapped in a <p class="g"> element. (I have no idea what g stands for. Google likes single-character names; it probably reduces their bandwidth costs.) Once we find a link that contains two hyphens, we can use "/ancestor::p[@class='g']" to get the surrounding paragraph, and then remove the entire search result in one shot.

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

	// ==UserScript==
	// @name			Hyphen Spam Remover
	// @namespace		http://diveintomark.org/projects/greasemonkey/
	// @description		remove search results with 2 or more hyphens in domain
	// @include			http://www.google.com/search*
	// ==/UserScript==

	var snapFilter = document.evaluate(
		"//a[starts-with(translate(translate(@href, 'http:', ''), " +
		"'.:abcdefghijklmnopqrstuvwxyz0123456789', ''), '//--')]" +
		"/ancestor::p[@class='g']", document, null,
		XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
	for (var i = snapFilter.snapshotLength - 1; i >= 0; i--) {
		var elmFilter = snapFilter.snapshotItem(i);
		elmFilter.parentNode.removeChild(elmFilter);
	}

Running the Hack

Go to http://www.google.com and search for buy cheap lortab site:.ru. (The site:.ru finds sites hosted in Russia. I have nothing against Russia per se, except that when I Wrote this, Google seemed to have already cracked down on most spammy domains in .com and .net, but I found several examples of such domains in .ru.) When you read this, your results will undoubtedly differ, but Figure 6-3 shows what I saw.

Figure 6-3. Google search with spammy results

Google search with spammy results

Now, install the script (Tools → Install This User Script), and refresh the Google search results page. My results were the same, except that the script removed the top search result, buy-cheap-lortab.on.ufanet.ru, as shown in Figure 6-4.

Figure 6-4. Google search, now with 10% less spam

Google search, now with 10% less spam

Hacking the Hack

The possibilities here are infinite. Don't want to ever see search results on microsoft.com? You could alter your search habits to include -microsoft.com in every search. Or you could let Greasemonkey do it for you:

	var snapFilter = document.evaluate(
		"//a[contains('microsoft.com')/ancestor::p[@class='g']",
		document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);

When I'm searching for a specific answer to a technical question, I often find the answer in a mailing list that has been publicly archived and indexed. Here's a variation of this hack that highlights search results that are likely to be part of an archived mailing list, as shown in Figure 6-5.

	var snapFilter = document.evaluate(
		"//a[contains(@href, 'pipermail') or " +
			"starts-with(@href, 'http://mail') or " +
			"starts-with(@href, 'http://list')]" +
		"/ancestor::p[@class='g']", document, null,
		XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
	for (var i = snapFilter.snapshotLength - 1; i >= 0; i--) {
		var elmFilter = snapFilter.snapshotItem(i);
		elmFilter.style.backgroundColor = 'silver';
	}

Figure 6-5. Search results with highlighted mailing list archives

Search results with highlighted mailing list archives

Find Similar Images

Explore the Web in a new way by finding other images of the same name.

I will be the first to admit that this hack has no practical purpose. I originally conceived it in an IRC channel, when someone posted a link to http://images.google.com/images?q=P5170003. That particular keyword is a filename used by a particular brand of digital camera. Some cameras generate filenames based on the date the photo was taken and a unique identifier within the camera; others simply use an incrementing identifier starting with 1. Many people take digital images and then simply publish them online, without giving the photo a more meaningful filename. The end result is that you can use Google Images to find a random selection of images published by different people. (This particular query finds photos taken on May 17, my wedding anniversary.)

Anyway, this hack converts all unlinked images into links to Google Images to find other random images with the same filename. If that sounds silly, that's because it is. It's also surprisingly fun, if you like that sort of thing.

The Code

This user script runs on all pages. It uses the document.images collection to find all the images on the page and wraps each of them in a link to http://images.google.com/images?q= plus the image filename. Firefox seriously dislikes replacing an element with another element that contains the original element, so we use the cloneNode method to make a copy of the original <img> element, put it in an <a> element, and then replace the original <img>.

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

	// ==UserScript==
	// @name		  Find Similar Images
	// @namespace	  http://diveintomark.org/projects/greasemonkey/
	// @description	  links images to find similar images on Google Image Search
	// @include		  http://*
	// @exclude		  http://*.google.tld/*
	// ==/UserScript==

	for (var i = document.images.length - 1; i >= 0; i--) {
		var elmImage = document.images[i];
		var usFilename = elmImage.src.split('/').pop();
		var elmLink = elmImage.parentNode;
		if (elmLink.nodeName != 'A') {
			var elmLink = document.createElement('a');
			elmLink.href = 'http://images.google.com/images?q=' +
				escape(usFilename);
			elmLink.title = 'Find images named ' + usFilename;
			var elmNewImage = elmImage.cloneNode(false);
			elmLink.appendChild(elmNewImage);
			elmImage.parentNode.replaceChild(elmLink, elmImage);
		}
	}

Running the Hack

After installing the user script (Tools → Install This User Script), visit http://randomness.org.uk/photos/index.cgi/months/may_2003. When you move your cursor over an image, you will see a tool tip displaying the filename of the image, as shown in Figure 6-6.

Each image on the page is now a link to a Google Images search for images of the same name. This can lead to some pretty random results, as shown in Figure 6-7.

Have fun exploring accidental cross-sections of the Web!

Figure 6-6. Image tool tips

Image tool tips

Figure 6-7. Other images named P5170003

Other images named P5170003

Search Wikipedia with Google Site Search

Replace Wikipedia's slow search engine with Google's lightning-quick site search.

I hack because I care. Really. I spend a lot of time on Google, and it shows in the number of hacks I've written that customize my experience of Google's services. The same applies to Wikipedia, the free (and freely licensed) online encyclopedia. I Hold Wikipedia in the highest regard, not only as a useful research tool, but as an example of a successful online community.

So, what's my beef with Wikipedia? Their site search is incredibly slow. I freely admit that I've been spoiled by Google. If I even bother using a site's internal search engine (as opposed to, say, searching Google with the site name as an additional keyword), I am instantly annoyed if the site search doesn't come back with useful results in under one second. Simon Willison shares my frustration, and he wrote this hack that modifies Wikipedia's search form to use Google Site Search instead of the site's internal search engine.

The Code

This user script runs on all Wikipedia pages. It uses hardcoded knowledge of Wikipedia's page structure to find the search form (<form id="searchform">), and then modifies the form's action attribute to point to Google Site Search. The search form has two submit buttons, so the script moves them around and directs one of them to Wikipedia's internal search, while the default button goes to Google Site Search.

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

	// ==UserScript==
	// @name		  Search Wikipedia with Google
	// @namespace	  http://simon.incutio.com/code/greasemonkey/
	// @description	  Alters Wikipedia search to use Google Site Search
	// @include		  http://*.wikipedia.org/*
	// ==/UserScript==

	// based on code by Simon Willison
	// and included here with his gracious permission

	var form = document.getElementById('searchform');
	var inputs = form.getElementsByTagName('input');
	var input = inputs[0];


	var go = inputs[1];
	var search = inputs[2];
	if (form && input && go && search) {
		// Move Go to the right
		form.appendChild(go);
		// Unbold it (by clearing its ID)
		go.id = '';
		// Search should be bold instead
		search.style.fontWeight = 'bold';
		// Update form to use Google
		form.action = 'http://www.google.com/search';
		input.name = 'as_q';
		// Add hidden q variable for site specific search
		var q = document.createElement('input');
		q.type = 'hidden';
		q.name = 'q';
		q.value = 'site:' + window.location.host;
		form.appendChild(q);
		// Set Go up to behave as normal
		go.addEventListener('click', function(event) {
			window.location.href = 'http://en.GoogleWikipedia searches usingwikipedia.org/wiki/Special' + 
				':Search?search=' + escape(input.value);
			event.preventDefault(); 
		}, true);
	}

Running the Hack

Before installing the user script, go to http://en.wikipedia.org, enter logical fallacies in the search box on the left, and click Search. The site search will churn and churn, and eventually take you to a page showing search results. Your mileage may vary, but for me, this search takes almost 30 seconds.

Now, install the user script (Tools → Install This User Script), and revisit or refresh http://en.wikipedia.org. The search form looks the same, except that the Go and Search buttons have been reversed. Now, when you type logical fallacies and click Search (or just press Enter), it will take you to Google's site search results of the Wikipedia site, as shown in Figure 6-8.

If you prefer to use Wikipedia's built-in search engine, you can do so by clicking Go instead of Search. The user script sets up this button to redirect to http://en.wikipedia.org/wiki/Special:Search?search=<your_search_keywords>. If Wikipedia can find an exact match, this will redirect to the result page; otherwise, it will display Wikipedia's search results page.

Figure 6-8. Google site search on wikipedia.org

Google site search on wikipedia.org

Link to Other Search Engines from Google

Make Google even more useful by adding links to competitors.

When Google was young and scrappy (circa 2001), it had an interesting feature. At the bottom of the search results page, Google offered links to try your search on the other major search engines of the day: AltaVista, Hot-bot, Excite, and a few others. The thinking behind it was that maybe you didn't find what you were looking for this time, but you should still try Google first on your next search.

Google is all grown up now, and they are the undisputed king of web search. Somewhere along the way to the top, they quietly dropped this feature. This hack brings it back.

The Code

This user script runs on Google search result pages. It retrieves the original query from the search form at the top of the page, then constructs a list of links to other search engines and inserts them at the top of the search results.

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

	// ==UserScript==
	// @name		   Try Your Search On
	// @namespace	   http://Googlelinking to other search enginesdiveintomark.org/projects/greasemonkey/
	// @description	   Link to competitors from searchingGoogleGoogle search results
	// @include		   http://www.google.tld/search*
	// ==/UserScript==
	
	// based on Butler
	// http://diveintomark.org/projects/butler/
	
	function getOtherWebSearches(q) {
		q = escape(q);
		return '' +
	'<a href="http://search.yahoo.com/search?p=' + q + '">Yahoo</a>, ' +
	'<a href="http://web.ask.com/web?q=' + q + '">Ask Jeeves</a>, ' +
	'<a href="http://alltheweb.com/search?q=' + q + '">AlltheWeb</a>, ' +
	'<a href="http://s.teoma.com/search?q=' + q + '">Teoma</a>, ' +
	'<a href="http://search.msn.com/results.aspx?q=' + q + '">MSN</a>, ' +
	'<a href="http://search.lycos.com/default.asp?query=' + q + '">Lycos</a>, '
	+
	'<a href="http://technorati.com/cosmos/search.html?url=' + q +
	'">Technorati</a>, ' +
	'<a href="http://feedster.com/search.php?q=' + q + '">Feedster</a>, ' +
	'<a href="http://daypop.com/search?q=' + q + '">Daypop</a>, ' +
	'<a href="http://bloglines.com/search?t=1&amp;q=' + q + '>Bloglines</a>';
	}

	function addOtherWebSearches() {
		var elmHeader = document.evaluate("//table[@bgcolor='#e5ecf9']",
			document, null, XPathResult.FIRST_ORDERED_NODE_TYPE,
			null).singleNodeValue;
		if (!elmHeader) return;
		var q = document.forms.namedItem('gs').elements.namedItem('q').value;
		var elmOther = document.createElement('div');
		var html = '<p style="font-size: small">Try your search on ';
		html += getOtherWebSearches(q);
		html += '</p>';
		elmOther.innerHTML = html;
		elmHeader.parentNode.insertBefore(elmOther, elmHeader.nextSibling);
	}

	addOtherWebSearches();

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://www.google.com and search for anything. At the top of the search results, you'll see a line with links to execute the same query on other search engines, as shown in Figure 6-9.

Figure 6-9. "Try your search on" other search engines

"Try your search on" other search engines

Hacking the Hack

Of course, Google can do more than just search the Web. It also lets you search for images. And of course there are lots of other image search engines. Some specialize in free images, others in commercial images. Some sites, such as Flickr (http://www.flickr.com) let you publish your own photos and search photos that other people have published. It sounds like Google Image Search needs a makeover.

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

	// ==UserScript==
	// @name			Try Your Search On
	// @namespace		http://diveintomark.org/projects/greasemonkey/
	// @description		Link to competitors from Google web and image search
	// @include			http://www.google.tld/search*
	// @include			http://images.google.tld/images*
	// ==/UserScript==
	
	function getOtherWebSearches(q) {
		q = escape(q);
		return '' +
	'<a href="http://search.yahoo.com/search?p=' + q + '">Yahoo</a>, ' +
	'<a href="http://web.ask.com/web?q=' + q + '">Ask Jeeves</a>, ' +
	'<a href="http://alltheweb.com/search?q=' + q + '">AlltheWeb</a>, ' +
	'<a href="http://s.teoma.com/search?q=' + q + '">Teoma</a>, ' +
	'<a href="http://search.msn.com/results.aspx?q=' + q + '">MSN</a>, ' +
	'<a href="http://search.lycos.com/default.asp?query=' + q + '">Lycos</a>, '
	+
	'<a href="http://technorati.com/cosmos/search.html?url=' + q +
	'">Technorati</a>, ' +
	'<a href="http://feedster.com/search.php?q=' + q + '">Feedster</a>, ' +
	'<a href="http://www.daypop.com/search?q=' + q + '">Daypop</a>, ' +
	'<a href="http://bloglines.com/search?t=1&amp;q=' + q + '>Bloglines</a>';
	}

	function addOtherWebSearches() {
		var elmHeader = document.evaluate("//table[@bgcolor='#e5ecf9']",
			document, null, XPathResult.FIRST_ORDERED_NODE_TYPE,
			null).singleNodeValue;
		if (!elmHeader) return;
		var q = document.forms.namedItem('gs').elements.namedItem('q').value;
		var elmOther = document.createElement('div');
		var html = '<p style="font-size: small">Try your search on ';
		html += Googlelinking to other search enginesgetOtherWebSearches(q);
		html += '</p>';
		elmOther.innerHTML = html;
		elmHeader.parentNode.insertBefore(elmOther, elmHeader.nextSibling);
	}

	function getOtherImageSearches(q) {
		q = escape(q);
		return '' +
	'<a href="http://images.search.yahoo.com/search/images?p=' + q +
	'">Yahoo</a>, ' +
	'<a href="http://pictures.ask.com/pictures?q=' + q +
	'">Ask Jeeves</a>, ' +
	'<a href="http://www.alltheweb.com/search?cat=img&q=' + q +
	'">AlltheWeb</a>, ' +
	'<a href="http://search.msn.com/images/results.aspx?q=' + q + '">MSN</a>, '
	+
	'<a href="http://www.picsearch.com/search.cgi?q=' + q + '">PicSearch</a>, '
	+
	'<a href="http://www.ditto.com/searchResults.asp?ss=' + q + '">Ditto</a>, '
	+
	'<a href="http://www.creatas.com/searchResults.aspx?' + 'searchString=' + q
	+
	'">Creatas</a>, ' +
	'<a href="http://www.freefoto.com/search.jsp?queryString=' + q +
	'">FreeFoto</a>, ' +
	'<a href="http://www.webshots.com/search?query=' + q + '">WebShots</a>, ' +
	'<a href="http://nix.larc.nasa.gov/search?qa=' + q + '">NASA</a>, ' +
	'<a href="http://www.flickr.com/photos/search/text:' + q + '">Flickr</a>';
		   return s;
	}
	function addOtherImageSearches() {
		var elmTable = document.evaluate( 
			"//a[starts-with(@href, '/images?q=')]/ancestor::table" +			
"[@width='100%'][@border='0'][@cellpadding='0'][@cellspacing='0']",
			document, null, XPathResult.FIRST_ORDERED_NODE_TYPE,
			null).singleNodeValue;
		if (!elmTable) { return; }
		var elmTR = document.createElement('tr');
		var q = document.forms.namedItem('gs').elements.namedItem('q').value;
		var html = '<td align="left"><span style="font-size: small">' +
			'Try your search on ';
		html += getOtherImageSearches(q);
		html += '</span></td>';
		elmTR.innerHTML = html;

		elmTable.appendChild(elmTR);
	}
	
	if (/^http:\/\/www\.searchingGooglegoogle\.[\w\.]+\/search/i.test(location.href)) {
		addOtherWebSearches();
	}
	else if (/^http:\/\/images\.google\.[\w\.]+\/images/i.test(location.href)) {
		addOtherImageSearches();
	}

After installing the user script (Tools → Install This User Script), go to http://images.google.com and search for something. At the top of the image results, you'll see a line with links to other image search engines and photo sites, as shown in Figure 6-10.

Figure 6-10. "Try your search on" other image search engines

"Try your search on" other image search engines

Just click the search engine you want to use, and you will jump straight to the search results page for the same keywords.

Prefetch Yahoo! Search Results

Automatically prefetch and cache the first search result on Yahoo! Web Search.

If you know how to use them properly, search engines are pretty darn good at finding exactly the page you're looking for. Google is so confident in its algorithm that it includes a hidden attribute in the search results page that tells Firefox to prefetch the first search result and cache it. You're probably going to click on the first result anyway, and when you do, it will load almost instantaneously, because your browser has already been there.

Yahoo! Web Search is pretty good, too, but it doesn't yet have this particular feature. So let's add it.

There are two important things about Yahoo! search results that you can discover by viewing the source on its search results page. First, the links of the search results each have a class yschttl. Yahoo! uses this for styling the links with CSS, but you can use it to find the links in the first place. A single XPath query can extract a list of all the links with the class yschttl, and the first one of those is the one we want to prefetch and cache.

The second thing you need to know is that the search results Yahoo! provides are actually redirects through a tracking script on rds.yahoo.com that records which link you clicked on. A sample link looks like this:

	http://rds.yahoo.com/S=2766679/K=gpl+compatible/v=2/SID=e/TID=F510_112/ 
	l=WS1/R=2/IPC=us/SHE=0/H=1/SIG=11sgv1lum/EXP=1116517280/*-http%3A//www.gnu. 
	org/licenses/gpl-faq.html

To save time and bandwidth, and to avoid skewing Yahoo!'s tracking statistics, this user script will extract the target URL out of the first search result link before requesting it. The target URL is always at the end of the tracking URL, after the *-, with characters such as colons (:) escaped into their hexadecimal equivalents. Here's the target URL in the previous example:

	http://www.gnu.org/licenses/gpl-faq.html

When I say "prefetch and cache," there is really only one step: prefetch. By default, Firefox automatically caches pages according to HTTP's caching directives and your browser preferences. For this script to have the desired effect, make sure that your browser preferences are set to enable caching pages. Open a new window or tab, go to about:config, and double-check the following preferences:

	* browser.cache.disk.enable			 /* should be "true" */
	* browser.cache.check_doc_frequency  /* should be 0, 2, or 3 */

Tip

about:config shows you all your browser preferences, even ones that are not normally configurable through the Options dialog. Type part of a preference name (such as browser.cache) in the Filter box to narrow the list of displayed preferences.

The Code

This user script will run on Yahoo! search results pages. It works by finding the first search result on the page and retrieving it. You might think that it would be easier to add a <link rel="prefetch"> to the page, which is how Google's prefetching works. Unfortunately, this does not work, because by the time the user script executes, Firefox has already prefetched all the links it's going to fetch for the page.

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

	// ==UserScript==
	// @name			Yahoo! Prefetcher
	// @namespace		http://diveintomark.org/projects/greasemonkey/
	// @description		prefetch first link on Yahoo! web search results
	// @include			http://search.yahoo.com/search*
	// ==/UserScript==

	var elmFirstResult = document.evaluate("//a[@class='yschttl']", document,
		null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
	if (!elmFirstResult) return;
	var urlFirstResult = unescape(elmFirstResult.href.replace(/^.*\*-/, ''));
	var oRequest = {
		method: 'GET',
		url: urlFirstResult,
		headers: {'X-Moz': 'prefetch',
				  'Referer': document.location.href}};
		GM_log('searchingprefetching Yahoo search resultsprefetching ' + urlFirstResult);
		GM_xmlhttpRequest(oRequest);

Running the Hack

To verify that the script is working properly, you'll need to clear your browser cache. You don't need to do this every time, just once to prove to yourself that the script is doing something. To clear your cache, go to the Tools menu and select Options; then, go to the Privacy tab and click the Clear button next to Cache.

Tip

You can also use the LiveHTTPHeaders extension to see exactly which URLs Firefox fetches. You can download the extension at http://livehttpheaders.mozdev.org/.

Now, install the user script from Tools → Install This User Script, and then go to http://search.yahoo.com and search for gpl compatible. The prefetching happens in the background after the page is fully loaded, so wait a second or two after the search results come up. There won't be any visible indication onscreen that Firefox is prefetching the link. You might see some additional activity on your modem or network card, but it's hard to separate this from the activity of loading the rest of the Yahoo! search results page.

Open a new browser window or tab and go to about:cache. This displays information about Firefox's browser cache. Under "Disk cache device," click List Cache Entries. You should see a key for http://www.gnu.org/philosophy/license-list.html. This is the result of Firefox prefetching and caching the first Yahoo! search results. Click that URL to see specific information about the cache entry, as shown in Figure 6-11.

Figure 6-11. Information about a prefetched page

Information about a prefetched page

Hacking the Hack

By now, you should realize that this prefetching technique can be used anywhere, with any links. Do you use some other search engine, perhaps a site-specific search engine such as Microsoft Developer's Network (MSDN)? You can apply the same technique to those search results.

For example, going to http://msdn.microsoft.com and searching for active accessibility takes you to a search results page at this URL:

	http://search.microsoft.com/search/results.
	aspx?qu=active+accessibility&View=msdn&st=b&c=0&s=1&swc=0

If you view source on the page, you will see that the result links are contained within a <div class="results"> tag. This means that the first result can be found with this XPath query:

	  var elmFirstResult = document.evaluate("//div[@class='results']//
	a[@href]",
		document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).
	singleNodeValue;

Unlike with Yahoo! search results, search result links are not redirected through a tracking script, so you will need to change this line:

	  var urlFirstResult = unescape(elmFirstResult.href.replace(/^.*\*-/, ''));

to this:

	  var urlFirstResult = elmFirstResult.href;

The rest of the script will work unchanged.

Browse the Web Through Google's Cache

Change links in cached pages to point to the cached version.

One of the nicest (and most controversial) features of Google's web search is its ability to show you a cached version of the page. This is useful if the original server is temporarily down or is just horrendously slow. It is also useful to see if the web publisher is playing tricks on Google to try to increase their search ranking, since the cache will show you the page that the site returned when Google's bots came a-crawling. The only downside of the Google cache is that links in the cached page point to the original site (which might still be unavailable, which was the reason you had to look at the cached version in the first place).

This hack modifies the cached pages that Google displays and adds links within the cached page to also point to Google's cache of the linked page.

The Code

This user script runs on Google cache pages. Google uses a variety of raw IP addresses to display cached pages, so we match on any IP address or domain name and simply look at the structure of the URL path and query parameters to determine whether we're looking at a cached page. If this causes false positives for you, you can exclude specific domains with an @exclude parameter.

There is one important thing to note in this code. Normally, I would use the document.links collection to get a list of all the links on the page. However, document.links is a dynamic collection. If you add a link to the page while iterating through the collection, you could end up in an infinite loop. Therefore, I use the document.evaluate function to return a static snapshot of all the links on the page. See "Master XPath Expressions" [Hack #8] for more information about static snapshots.

Save the following user script as google.cache.user.js:

	// ==UserScript==
	// @name		   Google Cache Continue
	// @namespace	   http://babylon.idlevice.co.uk/javascript/greasemonkey/
	// @description	   Convert Google cache links to also use Google cache
	// @include		   http://*/search?*q=cache:*
	// ==/UserScript==

	// based on code by Jonathon Ramsey
	// and included here with his gracious permission

	/* Modify these vars to change the appearance of the cache links */
	var cacheLinkText = 'cache';
	var cacheLinkStyle = "\
		a.googleCache {\

			font:normal bold x-small sans-serif;\
			color:red;\
			background-color:yellow;\
			padding:0 0.6ex 0.4ex 0.3ex;\
			margin:0.3ex;\
		}\
		a.searchingGoogle cached pagesgoogleCache:hover {\
			color:yellow;\
			background-color:red;\
		}\
		p#googleCacheExplanation {\
			border:1px solid green;\
			padding:1ex 0.5ex;\
			font-family:sans-serif;\
		}";

	addStyles(cacheLinkStyle);

	if (googleHasNoCache()) {
		addUncachedLink(urlPage); 
		return;
		}

		var arParts = window.location.href.match(/http:\/\/[^\/]*\/([^\+]*)(\
		+[^&]*)/);
		var urlPage = arParts[1];
		var sTerms = arParts[2];

		var bAlter = false;
		var snapLinks = document.evaluate('//a[@href]', document,
			null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
		for (var i = 0; i < snapLinks.snapshotLength; i++) {
			var elmLink = snapLinks.snapshotItem(i);
			if (bAlter && linkIsHttp(elmLink)) {
				addCacheLink(elmLink, sTerms, cacheLinkText);
			}
			if (isLastGoogleLink(elmLink)) {
				bAlter = true;
				addExplanation(elmLink, cacheLinkText);
			}
		}
		function addStyles(cacheLinkStyle) {
			var style = document.createElement('style');
			style.type = 'text/css'; 
			style.innerHTML = cacheLinkStyle;
			document.body.appendChild(style);
		}
		function googleHasNoCache() {
			return 0 == document.title.indexOf('Google Search: cache:'); 
		}
		function addUncachedLink(url) {

			var urlUncached = url.split('cache:')[1];
			var elmP = document.createElement('p');
			elmP.id = 'searchingGoogle cached pagesgoogleCacheExplanation';
			elmP.innerHTML = "<b>Uncached:</b> <a href='http://" + urlUncached +
				"'>" + urlUncached + '</a>';
			var suggestions = document.getElementsByTagName('blockquote')[0];
			document.body.replaceChild(elmP,
				suggestions.previousSibling.previousSibling); 
		}
		function linkIsHttp(link) {
			return 0 == link.href.search(/^http/); 
		}
		function isLastGoogleLink(elmLink) {
			return (-1 < elmLink.text.indexOf('cached text'));
		}
		function addExplanation(link, cacheLinkText) { 
			var p = document.createElement('p');
			p.id = 'googleCacheExplanation'; 
			p.innerHTML = "Use <a href='" +
				document.location.href +
				"' class='googleCache'>" +
				cacheLinkText +
				"</a> links Googledefaulting to cached pagesto continue using the Google cache.</a>";
			var tableCell = link.parentNode.parentNode.parentNode.parentNode;
			tableCell.appendChild(p); 
		}

		function addCacheLink(elmLink, sTerms, cacheLinkText) {
			var cacheLink = document.createElement('a');
			cacheLink.href = getCacheLinkHref(elmLink, sTerms);
			cacheLink.appendChild(document.createTextNode(cacheLinkText));
			cacheLink.className = 'googleCache';
			elmLink.parentNode.insertBefore(cacheLink, elmLink.nextSibling);
		}
		function getCacheLinkHref(elmLink, sTerms) {
			var href = elmLink.href.replace(/^http:\/\//, '');
			var fragment = '';
			if (hrefLinksToFragment(href)) {
				var arParts = href.match(/([^#]*)#(.*)/, href);
				href = arParts[1];
				fragment = '#' + arParts[2];
			}
			return 'http://www.google.com/search?q=cache:' + href + sTerms + fragment; 
		}
		
		function hrefLinksToFragment(href) {
			return (-1 < href.indexOf('#')); 
		}

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://www.google.com and search for "xml on the web" (including the quotes). At the time of this writing, the first result is for my article on O'Reilly's XML.com, titled "XML on the Web Has Failed," at http://www.xml.com/pub/a/2004/07/21/dive.html. Click the Cached link next to the first search result to see Google's cache of this article, as shown in Figure 6-12.

Figure 6-12. Cached copy of "XML on the Web Has Failed"

Cached copy of "XML on the Web Has Failed"

Each link in the article has been augmented with a "cache" link. Click the "cache" link next to the "Dive into XML" image, and it will take you to the cached copy of all the XML.com articles I've written, as shown in Figure 6-13.

Google does not keep cached copies of every page on the Internet. If a page is moved or deleted, it will eventually disappear from Google's cache. Or the publisher might use a <meta> element to tell Google not to cache a specific page. If you try to follow a link to a page that is not in Google's cache, Google will display an empty search results page informing you that the cached page could not be found, and the script will insert a link to the original page.

Figure 6-13. Cached copy of "Dive into XML" articles

Cached copy of "Dive into XML" articles

Add More Book Reviews to Google Print

Link to other book review and shopping sites after reading Google's book excerpts.

Google Print is a wonderful service, offering a fully searchable index into books before you buy them. But when you're ready to buy, there are a limited number of choices of online bookstores to which Google provides direct links.

Of course, there's more on the Web than just shopping sites. All Consuming (http://www.allconsuming.net) specializes in aggregating third-party reviews of books that people have recently blogged. This hack adds a link from Google Print to the page on All Consuming that displays reviews of the book you're interested in.

The Code

This user script runs on Google Print pages. It finds the ISBN of the book you're currently viewing and adds links to the pages on All Consuming just below the link to Froogle.

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

	// ==UserScript==
	// @name			Other Book Reviews
	// @namespace		http://diveintomark.org/projects/greasemonkey/
	// @description		add links to AllConsuming.net in GoogleGoogle PrintGoogle Print
	// @include			http://print.google.com/print*
	// ==/UserScript==

	// based on Butler
	// http://diveintomark.org/projects/butler/
	
	var elmFroogle = document.evaluate( "//a[contains(@href, 'froogle')]",
		document, null, XPathResult.FIRST_ORDERED_NODE_TYPE,
		null).singleNodeValue;
	if (!elmFroogle) return;
	var sISBN = unescape(elmFroogle.href).split('q=')[1].split('&')[0];

	var elmAllConsuming = document.createElement("a");
	elmAllConsuming.href = 'http://allconsuming.net/item/asin/' + sISBN;
	elmAllConsuming.style.display = "block";
	elmAllConsuming.innerHTML = "<br>Reviews @<br>AllConsuming.net";
	elmFroogle.parentNode.insertBefore(elmAllConsuming,
		elmFroogle.nextSibling);

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://print.google.com and search for Romeo and Juliet. Click the link under Book Results titled "Romeo and Juliet by William Shakespeare." In the list on the left, you will see a new link to All Consuming, as shown in Figure 6-14.

Hacking the Hack

Of course, there are other online bookstores and book services, too. ISBN.nu is an independent site that tracks book prices at several different online shopping sites. To add an additional link to look up the current book on ISBN.nu, add the following lines to the end of the script:

	var elmISBNnu = document.createElement("a");
	elmISBNnu.href = 'http://isbn.nu/' + sISBN;
	elmISBNnu.style.display = "block";
	elmISBNnu.appendChild(document.createTextNode("ISBN.nu"));
	elmFroogle.parentNode.insertBefore(elmISBNnu, elmFroogle.nextSibling);

Figure 6-14. AllConsuming.net link on Google Print

AllConsuming.net link on Google Print

Autocomplete Search Terms as You Type

Google can suggest your search terms before you even finish typing them.

It's true: Google is clairvoyant. It can guess what you're going to search for even before you've typed it. Well, maybe that overstates it. But it can certainly take an educated guess, based on the popularity and number of results of certain keywords.

Don't believe me? Visit http://www.google.com/webhp?complete=1 and start typing, and Google will autocomplete your query after you've typed just a few characters. This is insanely cool, and virtually nobody knows about it. And even people "in the know" need to visit a special page to use it. This hack makes this functionality work everywhere—even on the Google home page (http://www.google.com).

The Code

This user script runs on all Google pages, but it works only on pages with a search form. Of course, being Google, this is most pages, including the home page and web search result pages.

This hack doesn't do any of the autocompletion work itself. It relies entirely on Google's own functionality for suggesting completions for partial search terms, defined entirely in http://www.google.com/ac.js. All we need to do is create a <script> element pointing to Google's own code, and insert it into the page. Then, we tell Google to activate it by adding another <script> element that calls Google's own InstallAC function.

Save the following user script as google-autocomplete.user.js:

	// ==UserScript==
	// @name		 Google Autocomplete
	// @namespace    http://diveintomark.org/projects/greasemonkey/
	// @description  Autocomplete search keywords as you type
	// @include      http://*.google.tld/*
	// @exclude      http://*/*complete=1*
	// ==/UserScript==

	function getSearchBox(sFormName) {
		return document.forms.namedItem(sFormName);
	}

	function injectAC(sFormName) {
		var elmScript = document.createElement('script');
		elmScript.src = 'http://www.google.com/ac.js';
		document.body.appendChild(elmScript);
		var elmDriver = document.createElement('script');
		elmDriver.innerHTML = 'var elmForm = document.forms.namedItem("' +
		sFormName + '");\n' +
		'InstallAC(elmForm, elmForm.elements.namedItem("q"),' +
		'elmForm.elements.namedItem("btnG"), "search", "en");';
		document.body.appendChild(elmDriver);
	}

	var sFormName = 'f';
	var elmForm = getSearchBox(sFormName);
	if (!elmForm) {
		sFormName = 'gs';
		elmForm = getSearchBox(sFormName);
	}
	if (!elmForm) { return; }
	window.setTimeout(function() { injectAC(sFormName); }, 100);

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://www.google.com and start typing the word greasemonkey. After typing the first three letters, gre, you will see a drop-down menu with possible completions, as shown in Figure 6-15.

If you continue typing greasemonkey and then type a space, Google will suggest possible multiword searches, as shown in Figure 6-16.

Figure 6-15. Autocompletion of "gre" search

Autocompletion of "gre" search

Figure 6-16. Suggestions for multiword "greasemonkey" search

Suggestions for multiword "greasemonkey" search

Highlight Search Terms

When you click through to a page from a search engine, highlight the terms you originally searched for.

Have you ever searched for something on Google, then clicked through to a page and been unable to figure out why this page ranked so highly? Not only does it seem irrelevant, you can't even find the keywords you originally searched for! This hack tracks your search engine clickthroughs and highlights your original search keywords when you leave the results page for a given hit.

The Code

This user script runs on all pages except Google search result pages. The code is divided into three parts:

  1. The highlightWord function walks the DOM tree recursively and calls itself with each node, and then checks whether the current node is a block of text that contains a specific search term. If so, it wraps the word in a span tag and styles it with CSS to display with a yellow background.
  2. The highlightSearchKeywords function looks at the page you came from (document.referrer). If you came from a search results page, it parses out the keywords you originally searched for and calls highlightWord with each keyword.
  3. Finally, we add an event listener that calls highlightSearchKeywords after the page has completed loading.

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

	// ==UserScript==
	// @name		 Search Highlight
	// @namespace    http://www.kryogenix.org/code/
	// @description  highlight Googlehighlighting search terms in resulting pagessearch terms when coming a search engine
	// @include *
	// @exclude      http://www.google.tld/search*
	// ==/UserScript==

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

	function highlightWord(node, word) {
		if (node.hasChildNodes) {
			for (var hi_cn = 0; hi_cn<node.childNodes.length; hi_cn++) {
				highlightWord(node.childNodes[hi_cn], word);
			}
		}

		if (node.nodeType == Node.TEXT_NODE) {
			var tempNodeVal, tempWordVal, pn, nv, ni, before, docWordVal,
				after, hiwordtext, hiword;
			tempNodeVal = node.nodeValue.toLowerCase();
			tempWordVal = word.toLowerCase();
			if (tempNodeVal.indexOf(tempWordVal) != -1) {
				pn = node.parentNode;
				if (pn.className != "searchword") {
				nv = node.nodeValue;
				ni = tempNodeVal.indexOf(tempWordVal);
				before = document.createTextNode(nv.substr(0,ni));
				docWordVal = nv.substr(ni, word.length);
				after = document.createTextNode(nv.substr(ni+word.length));
				hiwordtext = document.createTextNode(docWordVal);
				hiword = document.createElement("span");
				hiword.className = "searchword";
				hiword.style.backgroundColor = 'yellow';
				
				hiword.style.color = 'black';
				hiword.appendChild(hiwordtext);
				pn.insertBefore(before, node);
				pn.insertBefore(hiword, node);
				pn.insertBefore(after, node);
				pn.removeChild(node);
				}
			}
		}
	}

	function Googlehighlighting search terms in resulting pageshighlightSearchKeywords() {
		var ref = document.referrer;
		if (ref.indexOf('?') == -1) { return; }
		var qs = ref.substr(ref.indexOf('?')+1);
		var qsa = qs.split('&');
		for (var i = 0; i < qsa.length; i++) {
			var qsip = qsa[i].split('=');
			if (qsip.length == 1) { continue; }
			if (qsip[0] == 'q') {
				var words = unescape(qsip[1].replace(/\+/g,' ')).split(/\s+/);
				for (var w = words.length - 1; w >= 0; w--) {
				highlightWord(document.body, words[w]);
				}
			}
		}
	}

	window.addEventListener('load', highlightSearchKeywords, true);

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://www.google.com and search for greasemonkey. Click through to the Greasemonkey home page (http://greasemonkey.mozdev.org/), and you will see the word Greasemonkey highlighted in several places, as shown in Figure 6-17.

The script can also handle multiword searches. Go to http://www.google.com and search for download firefox. Click through to the Firefox home page (http://www.mozilla.org), and you will see both download and firefox highlighted in several places, as shown in Figure 6-18.

Tip

The large "Get Firefox" banner near the top is not highlighted because this text is actually an image.

Figure 6-17. Greasemonkey home page with "Greasemonkey" highlighted

Greasemonkey home page with "Greasemonkey" highlighted

Figure 6-18. Firefox home page with "download" and "firefox" highlighted

Firefox home page with "download" and "firefox" highlighted

Hacking the Hack

It's easy to extend this script to handle search engines other than Google. Whenever you click from one page to another, you have access to the referring page in document.referrer. (That's why the script works in the first place.) Yahoo! Web Search uses a slightly different URL on its result pages. On Google, your search keywords are stored in the q parameter; on Yahoo!, they are stored in the p parameter. To highlight search terms when coming from either Google or Yahoo!, change this line:

	if (qsip[0] == 'q') {

to this:

	if (qsip[0] == 'q' || qsip[0] == 'p') {

You might also want to exclude Yahoo! search result pages by adding this line to the script's metadata section:

	// @exclude http://search.yahoo.com/*

This prevents the script from highlighting your search terms on the second page of Yahoo!'s search results.

Remember Recent Google Searches

Track what you search for and which search results you follow.

Google recently added yet another beta service: My Search History (http://www.google.com/searchhistory/). In a nutshell, you log into your Google account, and My Search History remembers which keywords you search for and which search results you end up following. A nice idea, but it has some limitations that disappointed me when I tried it. My Search History isn't immediately available on the Google home page. Also, clicking a previous search simply reexecutes the search, instead of actually taking me to the result I followed last time. How is that useful? I remember what I searched for; what I want to know is what I found!

This hack lets me do what I had hoped the "My Search History" tool would do.

The Code

This user script runs on all Google pages. The code itself breaks down into three distinct parts:

  1. The SavedSearches function and associated prototype methods are used to create a persistent array—i.e., an Array class that saves its data to the Firefox preferences database.
  2. The getCurrentSearchText, addCurrentSearch, clearSavedSearches, and injectRecentSearches functions handle the basic operations of the script. Whenever you execute a Google search, the script adds your keywords to its persistent array, and then alters the search results page to include a list of your recent searches.
  3. The trackClick function is where the real magic happens. On search result pages, we register trackClick as a global onclick event handler. When you click on anything on the search results page, trackClick is called. It looks at where you clicked, and if you clicked on a search result, it stores the title and URL of the link before following it.

The end result is seamless: you search, click a search result, and visit the result page. But invisibly, behind the scenes, the user script has tracked and stored your every movement.

Warning

As this hack demonstrates, user scripts have the potential to track virtually everything you do on the Web. This includes what you search for, where you go, how long you stay, and even the passwords you enter on secure sites. Combine this with the ability of user scripts to send data to any site at any time, with the GM_xmlhttpRequest function, and you have a recipe for catastrophic privacy violations.

None of the hacks in this book compromise your privacy in any way. For example, this script only stores data in your local Firefox preference database, and you can clear it at any time. But you need to be aware that third-party user scripts can do a great deal of damage. Install only scripts that you personally understand, or from sources you trust. This is good advice for any type of download.

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

	// ==UserScript==
	// @name         Recent Searches
	// @namespace	 http://diveintomark.org/projects/greasemonkey/
	// @description  remember and display recent Google searches
	// @include		 http://www.google.com/*
	// ==/UserScript==

	function SavedSearches() {
		var iCount = GM_getValue('count') || 0;
		for (var i = 0; i < iCount; i++) {
			this.push({
				"searchtext": GM_getValue('searchtext.' + i, ''),
				"searchresult": GM_getValue('searchresult.' + i, '')});
		}
	}
	SavedSearches.prototype = new Array();

	SavedSearches.prototype.find = function(sSearchText) {
		for (var i = this.length - 1; i >= 0; i--) {
			if (this[i] == sSearchText) {
				return i;
			}
		}
		return -1;
	};

	SavedSearches.prototype.append = function(sSearchText) {
		GM_setValue('searchtext.' + this.length, sSearchText);
		this.push({"searchtext": sSearchText});
		GM_setValue('count', this.length);
	};

	var arSavedSearches = new SavedSearches();

	function getCurrentSearchText() {
		var elmForm = document.forms.namedItem('gs');
		if (!elmForm) { return; }
		var elmSearchBox = elmForm.elements.namedItem('q');
		if (!elmSearchBox) { return; }
		var sKeyword = elmSearchBox.value;	
		if (!sKeyword) { return; }
		return sKeyword;
	}

	function addCurrentSearch() {
		var sCurrentSearchText = getCurrentSearchText();
		if (!sCurrentSearchText) { return; }
		var sLastSearch = null;
		if (arSavedSearches.length) {
			sLastSearch = arSavedSearches[arSavedSearches.length - 1];
		}
		if (sLastSearch &&
			(sLastSearch['searchtext'] == sCurrentSearchText)) {
			return;
		}
		arSavedSearches.append(sCurrentSearchText);
	}

	function clearSavedSearches() {
		for (var i = 0; i < arSavedSearches.length; i++) {
			GM_setValue('searchtext.' + i, '');
			GM_setValue('searchresult.' + i, '');
		}
		GM_setValue('count', 0);
		arSavedSearches = new SavedSearches();
		var Googleremembering recent searcheselmRecentSearches = document.getElementById('recentsearcheslist');
		if (elmRecentSearches) {
			elmRecentSearches.innerHTML = '';
		}			
	}

	function injectRecentSearches() {
		if (!arSavedSearches.length) { return; }
		var elmFirst = document.evaluate("//table[@bgcolor='#e5ecf9']",
			document, null, XPathResult.FIRST_ORDERED_NODE_TYPE,
			null).singleNodeValue;
		if (!elmFirst) {
			elmFirst = document.evaluate("//form[@name='f']",
				document, null, XPathResult.FIRST_ORDERED_NODE_TYPE,
				null).singleNodeValue;
		}
		if (!elmFirst) { return; }
		var htmlRecentSearches = '<p style="font-size: small">Googleremembering recent searchesRecent searches:
';
		var iDisplayedCount = 0;
		for (var i = arSavedSearches.length - 1;
			(iDisplayedCount <10) && (i >= 0); i--) {
			var oSearch = arSavedSearches[i];
			if (!oSearch['searchresult']) { continue; }
			var sSearchResult = oSearch['searchresult'];
			var iSpacePos = sSearchResult.indexOf(' ');
			var sHref = sSearchResult.substring(0, iSpacePos);
			var sTitle = sSearchResult.substring(iSpacePos + 1);
			htmlRecentSearches += '<a href="' + sHref + '" title="' +
				sTitle + '">' + oSearch['searchtext'] + '</a> &middot; ';
			iDisplayedCount++;
		}
		if (!iDisplayedCount) { return; }
		htmlRecentSearches += '[<a id="clearsavedsearches" ' +
			'title="Clear saved searches" href="#">clear</a>]</p>';
		var elmWrapper = document.createElement('div');
		elmWrapper.id = "recentsearcheslist";
		elmWrapper.innerHTML = htmlRecentSearches;
		elmFirst.parentNode.insertBefore(elmWrapper, elmFirst.nextSibling);
		window.addEventListener('load', function() {
			var elmClearLink = document.getElementById('clearsavedsearches');
			elmClearLink.addEventListener('click', clearSavedSearches, true);
		}, true);
	}

	function trackClick(event) {
		var sHref, sTitle;
		var elmTarget = event.target;
		while ((elmTarget.nodeName != 'A') &&
				(elmTarget.nodeName != 'BODY')) {
			elmTarget = elmTarget.parentNode;
		}
		if (elmTarget.nodeName != 'A') { return; }
		var elmParent = elmTarget.parentNode;
		while ((elmParent.nodeName != 'P') &&
			(elmParent.nodeName != 'BODY')) {
			elmParent = elmParent.parentNode;
		}
		if (elmParent.nodeName != 'P') { return; }
		if (elmParent.getAttribute('class') != 'g') { return; }	
		sHref = elmTarget.href;
		sTitle = elmTarget.textContent;
		var iSearchIndex = arSavedSearches.find(getCurrentSearchText());
		if (iSearchIndex == -1) {
			addCurrentSearch();
			iSearchIndex = arSavedSearches.length - 1;
		}
		GM_setValue('searchresult.' + iSearchIndex,
				sHref + ' ' + sTitle);
	}

	if (/^\/search/.test(location.pathname)) {
		injectRecentSearches();
		addCurrentSearch();	
		document.addEventListener('click', trackClick, true);
	} else if (/^\/$/.test(location.pathname)) {
		injectRecentSearches();
	}			

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://www.google.com and search for something. Click on an interesting search result. Lather, rinse, and repeat. Each time you revisit the Google home page, you will see a growing list of your recent searches, as shown in Figure 6-19. Clicking on a recent search term will take you to the same link you followed when you originally executed the search.

Figure 6-19. Recent searches on the Google home page

Recent searches on the Google home page

You will also see the list of recent searches on the search results page itself. Hovering over a recent search displays the title of the linked page, as shown in Figure 6-20.

Figure 6-20. Recent searches on Google's search results page

Recent searches on Google's search results page

Hacking the Hack

The links in the recent searches list go directly to the search result you clicked when you searched. But what if you want to rerun the search and go somewhere else? That's easy enough. In the injectRecentSearches function, find these two lines:

	var sHref = sSearchResult.substring(0, iSpacePos);
	var sTitle = sSearchResult.substring(iSpacePos + 1);

And change them like this:

	var sHref = 'http://www.google.com/search?q=' + escape(sSearchResult); 
	var sTitle = 'previously found ' + sSearchResult.substring(iSpacePos + 1) + 
		'\n' + sSearchResult.substring(0, iSpacePos);

Now, if you hover over a link in the recent searches list, the tool tip will display the title and URL of the page you went to last time. If you click the link, it will reexecute the search so that you can choose a different search result this time.

Add Keyboard Shortcuts to Google Search Results

If you search frequently and type as quickly as you think, you'll appreciate this keyboard-only hack.

I love Google. I use Google 50 times a day…literally. I actually used Google once to look up my own phone number. I was placing an order over the phone when the customer service representative asked me for my home phone, and I totally drew a blank. Has that ever happened to you? I should really get one of those weblog thingies all the kids are talking about, so I can regurgitate personal anecdotes like this in a virtual medium, instead of wasting all this paper. But I digress.

As I was saying, I search a lot, and I type very quickly. And if I'm looking for very specific things, and Google is so very good at finding them, I usually find what I'm looking for in the first page of search results, which means that this hack is perfect for me, because it numbers the search results and lets me follow them without moving my hands off the keyboard.

The Code

This user script runs on all Google pages. It uses an XPath query to find all the search results (they're each wrapped in a <p class="g"> element), and then inserts red numbers beside each search result, from 1 to 9, then 0 for the 10th result. The last line of the script ties it all together by registering a global onkeydown event handler that checks whether you typed a number, and if so, finds the associated search result and follows the link.

Save the following user script as google-searchkeys.user.js:

	// ==UserScript==
	// @name			Google Searchkeys
	// @namespace		http://www.imperialviolet.org
	// @description		Adds one-press access keys to Google search results
	// @include			http://www.google.*/search*
	// ==/UserScript==
	
	// based on code by Adam Langley
	// and included here with his gracious permission
	
	var results = document.evaluate("//p[@class='g']", document, null,
		XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
	var counter = 1;
	var querybox = document.evaluate("//input[@name='q']", document, null,
		XPathResult.ORDERED_NODE_ITERATOR_TYPE, null).iterateNext();
	var next_nodes = document.evaluate(
		"//a[span[@class='b' and text()='Next']]",
		document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
	var prev_nodes = document.evaluate(
		"//a[span[@class='b' and text()='Previous']]",
		document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
	var nextlink = null;
	var prevlink = null;
	if (next_nodes.snapshotLength) {
		nextlink = next_nodes.snapshotItem(0).getAttribute('href');
	}
	if (prev_nodes.snapshotLength) {
		prevlink = prev_nodes.snapshotItem(0).getAttribute('href');
	}
	prev_nodes = next_nodes = null;
	var links = new Array();
	for (var i = 0; i < results.snapshotLength; ++i) {
		var result = results.snapshotItem(i);
		links.push(result.firstChild.nextSibling.getAttribute("href"));
		var newspan = document.createElement("span");
		newspan.setAttribute("style", "color:red; font-variant: small-caps;");
		newspan.appendChild(document.createTextNode('' + counter++ + ' '));
		result.insertBefore(newspan, result.firstChild);
	}
	results = null;

	function keypress_handler(e) {
		if (e.ctrlKey || e.altKey || e.metaKey) { return true; }
		if (e.target.nodeName == 'INPUT' && e.target.name == 'q') {
			return true;
		}
		var keypressed = String.fromCharCode(e.which);
		if (nextlink && (keypressed == 'l' || keypressed == 'L' ||
				 keypressed == '.')) {
			if (e.shiftKey) {
				window.open(nextlink,'Search Results','');
			} else {
				document.location.href = nextlink;
			}
			return false;
		}
		if (prevlink && (keypressed == 'h' || keypressed == 'H' ||
				 keypressed == ',')) {
			if (e.shiftKey) {
				window.open(prevlink,'Search Results','');
			} else {
				document.location.href = prevlink;
			}
			return false;
		}

		if (keypressed <'0' || keypressed > '9') {
			return true;
		}

		var resnum = e.which - "0".charCodeAt(0);
		if (resnum == 0) {
			resnum = 10;
		}

		if (e.shiftKey) {
			window.open(links[resnum - 1],'Search Results','');
		} else {
			document.location.href = links[resnum - 1];
		}
		return false;
	}

	document.addEventListener('keydown', keypress_handler, false);

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://www.google.com and search for something, such as ipod. Next to each Google search result, you will see a red number, as shown in Figure 6-21. Typing that number will redirect you to that linked search result.

Figure 6-21. Search results with keyboard shortcuts

Search results with keyboard shortcuts

Hacking the Hack

Besides numbering the search results, this script has some hidden features. On a search results page, type L to see the next 10 search results, or type H to go back to the previous 10 search results.

In addition, you can hold the Shift key while typing a number to open the search result in a new window. This also works for jumping between previous and next pages of search results: Shift-L opens the next 10 results in a new window, and Shift-H opens the previous 10 results in a new window.

Use Recent Searches and Google SearchKeys Together

Recent Searches needs an update to play nicely with Google SearchKeys.

I stumbled onto this hack by accident. I had been running Recent Searches [Hack #57] for a few weeks, and I heard about Google SearchKeys [Hack #58] on the Greasemonkey mailing list. I went to install it and immediately fell in love with it, but after a few searches, I realized that my recent searches list wasn't being updated anymore.

After investigating, I discovered that, because of the way the Google SearchKeys user script works, it was never calling the onclick handler I Had defined for the search result links. Instead, Google SearchKeys simply parsed out the URL of each search result link and assigned it to window.location.href, thus loading the result page and creating the illusion of "following" the link. The illusion was almost perfect, except that my Recent Searches script was assuming that the only way to follow a result link was to click it (or navigate to it with the keyboard and press Enter, but either way would trigger the link's onclick handler).

After a little intensive research, I had a solution so ingenious that my editor agreed it was worthy of its own hack. JavaScript has a feature called watchpoints. On every object, you can set a watchpoint on one of the object's properties. When that property is about to be changed, the JavaScript engine will call a callback function of your choosing, with the property name, the old value, and the new value.

By setting watchpoints on document.location, document.location.href, window. location, and window.location.href, we can notice when a script (such as Google SearchKeys) is trying to move to a different page programmatically. We can save the URL of the new page to our recent changes database and then let the script go about its merry way.

The Code

This user script runs on all Google pages, because it displays the list of recent searches on the Google home page as well as on search results pages. The bulk of this script is the same as "Remember Recent Google Searches" [Hack #57]; the changes are listed in boldface.

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

	// ==UserScript==
	// @name			Recent Searches
	// @namespace		http://diveintomark.org/projects/greasemonkey/
	// @description		remember and display recent Google searches
	// @include			http://www.google.*/search*
	// ==/UserScript==
	
	// based on code by Adam Langley
	// and included here with his gracious permission
	// http://www.imperialviolet.org/page24.html

	function SavedSearches() {
		var iCount = GM_getValue('count') || 0;
		for (var i = 0; i <iCount; i++) {
			this.push({
				"searchtext": GM_getValue('searchtext.' + i, ''),
				"searchresult": GM_getValue('searchresult.' + i, '')});
		}
	}

	SavedSearches.prototype = new Array();

	SavedSearches.prototype.find = function(sSearchText) {
		for (var i = this.length - 1; i >= 0; i--) {
			if (this[i] == sSearchText) {
				return i;
			}
		}
		return -1;
	};

	SavedSearches.prototype.append = function(sSearchText) {
		GM_setValue('searchtext.' + this.length, sSearchText);
		this.push({"searchtext": sSearchText});
		GM_setValue('count', this.length);
	};

	var arSavedSearches = new SavedSearches();

	function getCurrentSearchText() {
		var elmForm = document.forms.namedItem('gs');
		if (!elmForm) { return; }
		var elmSearchBox = elmForm.elements.namedItem('q');
		if (!elmSearchBox) { return; }
		var sKeyword = elmSearchBox.value;
		if (!sKeyword) { return; }
		return sKeyword;
	}

	function addCurrentSearch() {
		var sCurrentSearchText = getCurrentSearchText();
		if (!sCurrentSearchText) { return; }
		var sLastSearch = null;
		if (arSavedSearches.length) {
			sLastSearch = arSavedSearches[arSavedSearches.length - 1];
		}
		if (sLastSearch &&
			(sLastSearch['searchtext'] == sCurrentSearchText)) {
			return;
		}
		arSavedSearches.append(sCurrentSearchText);
	}

	function clearSavedSearches() {
		for (var i = 0; i <arSavedSearches.length; i++) {
			GM_setValue('searchtext.' + i, '');
			GM_setValue('searchresult.' + i, '');
		}
		GM_setValue('count', 0);
		arSavedSearches = new SavedSearches();
		var elmRecentSearches = document.getElementById('recentsearcheslist');
		if (elmRecentSearches) {
			elmRecentSearches.innerHTML = '';
		}
	}

	function injectRecentSearches() {
		if (!arSavedSearches.length) { return; }
		var elmFirst = document.evaluate("//table[@bgcolor='#e5ecf9']",
			document, null, XPathResult.FIRST_ORDERED_NODE_TYPE,
			null).singleNodeValue;
		if (!elmFirst) {
			elmFirst = document.evaluate("//form[@name='f']",
				document, null, XPathResult.FIRST_ORDERED_NODE_TYPE,
				null).singleNodeValue;
			}
			if (!elmFirst) { return; }
			var htmlRecentSearches = '<p style="font-size: small">Googlerecent searches and Google SearchKeysRecent searches:
';
			var iDisplayedCount = 0;
			for (var i = arSavedSearches.length - 1;
				(iDisplayedCount < 10) && (i >= 0); i--) {
				var oSearch = arSavedSearches[i];
				if (!oSearch['searchresult']) { continue; }
				var sSearchResult = oSearch['searchresult'];
				var iSpacePos = sSearchResult.indexOf(' ');
				var sHref = sSearchResult.substring(0, iSpacePos);
				var sTitle = sSearchResult.substring(iSpacePos + 1);
				Googlerecent searches and Google SearchKeyshtmlRecentSearches += '<a href="' + sHref + '" title="' +
				sTitle + '">' + oSearch['searchtext'] + '</a> &middot; ';
				iDisplayedCount++;
			}
			if (!iDisplayedCount) { return; }
			htmlRecentSearches += '[<a id="clearsavedsearches" ' +
				'title="Clear saved searches" href="#">clear</a>]</p>';
			var elmWrapper = document.createElement('div');
			elmWrapper.id = "recentsearcheslist";
			elmWrapper.innerHTML = htmlRecentSearches;
			elmFirst.parentNode.insertBefore(elmWrapper, elmFirst.nextSibling);
			window.addEventListener('load', function() {
				var elmClearLink = document.getElementById('clearsavedsearches');
				elmClearLink.addEventListener('click', clearSavedSearches, true);
			}, true);
		}

		function trackClick(event) {
			var sHref, sTitle;
			if (typeof(event) == 'string') {
				sHref = event;
				sTitle = '';
			} else {
				var elmTarget = event.target;
				while ((elmTarget.nodeName != 'A') &&
				(elmTarget.nodeName != 'BODY')) {
				elmTarget = elmTarget.parentNode;
				}
				if (elmTarget.nodeName != 'A') { return; }
				var elmParent = elmTarget.parentNode;
				while ((elmParent.nodeName != 'P') &&
				(elmParent.nodeName != 'BODY')) {
				elmParent = elmParent.parentNode;
				}
				if (elmParent.nodeName != 'P') { return; }
				if (elmParent.getAttribute('class') != 'g') { return; }
				sHref = elmTarget.href;
				sTitle = elmTarget.textContent;
			}
			var iSearchIndex = arSavedSearches.find(getCurrentSearchText());
			if (iSearchIndex == -1) {
				addCurrentSearch();
				iSearchIndex = arSavedSearches.length - 1;
			}
			GM_setValue('searchresult.' + iSearchIndex,
				sHref + ' ' + sTitle);
		}

		function watchLocation(sPropertyName, sOldValue, sNewValue) {
			trackClick(sNewValue);
			return sNewValue;
		}

		if (/^\/search/.test(window.location.pathname)) {
			injectRecentSearches();
			addCurrentSearch();
			document.addEventListener('click', trackClick, true);
			var unsafeDocument = document.wrappedJSObject || document;
			unsafeDocument.watch('location', watchLocation);
			unsafeDocument.location.watch('href', watchLocation);
			unsafeWindow.watch('location', watchLocation);
			unsafeWindow.location.watch('href', watchLocation);	
		} else if (/^\/$/.test(window.location.pathname)) {
			injectRecentSearches();
		}

Running the Hack

As I mentioned before, the only reason for this hack is to get two previous hacks to play nicely with each other. After you install this script (Tools → Install This User Script), you also need to install google-searchkeys.user.js from "Add Keyboard Shortcuts to Google Search Results" [Hack #58].

Now, go to http://www.google.com and search for anything. In the search results page, you will see the numbers to the left of each search result, as shown in Figure 6-22.

Tip

You won't see any recent searches unless you've previously installed the Recent Searches hack and executed a search.

Figure 6-22. Google search with keyboard shortcuts

Google search with keyboard shortcuts

Click through to one of the search result pages by typing the number next to the link.

Go back to http://www.google.com. Below the search box is the list of recent searches, as shown in Figure 6-23.

Figure 6-23. Recent searches executed with the keyboard

Recent searches executed with the keyboard

The list of recent searches includes the result link you just followed by typing the Google SearchKeys keyboard shortcut.

Personal tools