Delicious Link Tooltop

Shows Delicious info for the target page in a tooltip when you hover over a link.

// ==UserScript==
// @name           Delicious Link Tooltop
// @namespace      GPM
// @description    Shows Delicious info for the target page in a tooltip when you hover over a link.
// @downstreamURL  http://userscripts.org/scripts/source/60831.user.js
// @version        1.0.0
// @include        *
// @exclude        http://facebook.com/*
// @exclude        https://facebook.com/*
// @exclude        http://*.facebook.com/*
// @exclude        https://*.facebook.com/*
// @exclude        http://images.google.*/*
// @exclude        http://*.google.*/images?*
// @grant          none
// ==/UserScript==
// Some older browsers may need JSON, but this link is broken:
// @require        http://json.org/json2.js



// == Config ==

var showTooltips      = true;    // Make little info boxes pop up when you hover over a link.
var lookupCurrentPage = true;    // Display count for this page shown in top-right corner.
var annotateAllLinks  = false;   // Lookup every link on the page (almost), and display its count in a blue box after the link.
                                 // Delicious may occasionally block this for spamming (temporarily).

var enableJSONPonHTTPS = false;  // JSONP only works on https pages in Chrome if the user confirms each page.  This is a hassle so it is disabled by default.

var maxCacheSize = 2400;                 // Global cache limit for Greasemonkey/GM_setValue.
var maxCacheSizeForLocalStorage = 1000;  // Per-site cache limit for Chrome/localStorage.

var showProgress = true;   // Highlights each link in yellow while we are making a delicious request on it.  Messy but informative if lookup fails!
var logRequests  = false;
var logResponses = true;
var displayCleanupInTitle = true;   // To debug if (display when) the cleanup is lagging your browser.



/* == Changelog ==

 2012-02-20  Prevented interfering with top bar of Google.

 2012-02-05  Added enableJSONPonHTTPS.  If you turn it on in Chrome, you will
             need to answer the browser's "Load Insecure Content?" popup.
             (Do this only if you trust Delicious.com!)  Visit github to test.

             Does not affect Firefox.  Greasemonkey's GM_xmlhttpRequest still
             works fine on https pages.

 2012-01-31  Noticed that lookups are not working on https pages.  Surprised I
             hadn't seen this before.

 2012-01-30  Uploaded new version to userscripts.org.

 2011-11-??  I think I fixed lookup bugs by generating the md5sum here (big lib).
             Earlier versions without the md5sum may still be usable!

*/



// TODO: Move this to its own script!!
if (this.GM_addStyle) { GM_addStyle("a:visited { color: #440066; }"); }



var secondRun = ( window.DLT_loaded ? true : (window.DLT_loaded=true) && false );

// I want to switch annotateAllLinks on if I run the userscript twice.
if (secondRun) {
	annotateAllLinks = true;
	lookupCurrentPage = true;
}



//// NOTES for Delicious Network Add:
// GET http://delicious.com/settings/networkadd?networkadd=EHKNIGHT&.crumb=7FsAGAcj8szIc7Pt_37ua1qsjMM-
// GET http://delicious.com/network?add=EHKNIGHT
// GET http://delicious.com/EHKNIGHT?networkaction=1
//// When user hits OK:
// POST http://delicious.com/settings/networkadd?.crumb=7FsAGAcj8szIc7Pt_37ua1qsjMM-&jump=%2FEHKNIGHT%3Fnetworkaction%3D1&network-action-ok=&networkadd=EHKNIGHT
// redirects to: http://delicious.com/EHKNIGHT?networkaddconfirm=EHKNIGHT

// TODO: Don't annotate images, duplicates of last, or anything on delicious.com

//// Some old stats:
// Cleanup of cache size 2048 took 27 seconds in Firefox 4.
// Cleanup of cache size 1024 took 6.7 seconds in Firefox 4.
// In Chrome 512 means my cache cleanup takes 2-5 seconds.




// log = (unsafeWindow.console && unsafeWindow.console.log) ? unsafeWindow.console.log : GM_log;
// log = GM_log;
function log(x,y,z) {
	if (this.GM_log) {
		GM_log(x,y,z);
	} else if (this.console && console.log) {
		console.log(x,y,z);
	}
	window.status = ""+x+" "+y+" "+z;
	// window.status = Array.prototype.join(arguments," "); // Didn't work for me (GM in FF)
}


/*
// == Silly scrolling logger == //
var scrollText = "";

function showScroller() {
	window.status = scrollText.substring(0,240).replace(/\s/,' ','g');
}

function animateScroller() {
	showScroller();
	if (scrollText.length > 0)
		setTimeout(animateScroller,200);
	scrollText = scrollText.substring(10);
}

var oldLog = log;
log = function(x) {
	oldLog(x);
	if (scrollText == "") // if not, there is probably already a timeout running
		setTimeout(animateScroller,2000);
	scrollText = scrollText + "     " + x;
	showScroller();
};
*/



// == Notes == //

// DONE: Rename script "Get Link Meta" or "Get Delicious Info for Links" or
// "Delicious Link Tooltip" with an accidental typo then upload it.

// Some code adapted from Hover Links userscript.
// Thanks to: http://delicious.com/developers
// The delicious JSON url we use is:
//   http://feeds.delicious.com/v2/json/urlinfo?url=http://www.google.co.uk/

// DONE: Since bookmarklets can't make CD-XHR, we could use a JSONP proxy like:
//   http://hwi.ath.cx/json_plus_p?url=http://feeds.delicious.com/... ?

// DONE: onclick should cancel the lookup (user has followed link, no need to query)

// DONE: If the XHR request calls callback *after* we have done mouseout, link still
// appears.  Related problem if two requests are being made and neither has run
// callback yet.  Recommend setting/unsetting/checking var targettingLink =
// null or elem;  Or maybe we can use stillFocused.

// DONE: We prevent a second XHR being made if one is already running for a
// given link, by saving the "working..." string in the dataCache.

// DONE: Give all tags an automatic color by hash, but not too vivid.  May help
// us to track common tags.

// TODO: Many Mediawiki sites are configured to show a tooltip for external
// links, and this can conflict with our tooltip!
// We could try to stop this in all cases, by doing evt.preventDefault() on all
// mouseovers/outs, but that is a bit strong.
// At least a fix for the Mediawiki problem would make us (me) happy.
// Tried 100x larger zIndex but no success.
// Maybe it's the browser's display of the alt or title property.

// DONE: Activate for links added to the document later.  Should add a
// DOMNodeInserted event listener.  Or maybe neater, add listeners to the top
// of the doc, but have them only activate on A elements.

// I made some of the info in the tooltip into links, but to access them we
// have to allow mouseover the tooltip without it disappearing.  I tried to
// implement that but things haven't been 100% smooth since...

// DONE: Sometimes if we move quickly over to the link, no tooltip will appear.
// DONE: The tooltip should appear wherever we last mousemoved-ed over the
// link, (current cursor pos) rather than where we mouseover-ed (entered it).
// HMMM it seems to me that this is happening when we mouseover the <EM> part
// of a Google result but works fine on the non-EM parts.  So maybe we should
// be using capture?

// DONE: The right-floating popBar/Cont is working for Chrome and Konq, but not FF!
// Forget CSS float, I used a TABLE and all works fine.  :)
// TODO: Tidy up the aftermath.
// Unfortunately the table appears to override our popWidth, but maybe we don't
// need/want that anymore anyway.

// TODO: Automatic annotation's decision about which links to scan could be improved.
// e.g. some search ? links could be checked if they are short.  Some same-host links
// we may also want to checked.  But some URLs we definitely won't want to, e.g. if
// they appear to contain a (large) session ID.

// TODO BUG: The addition of the blue number "flag" by annotateAllLinks will
// often cause parts of the page to grow, breaking the intended CSS layout.  We
// should seek to mitigate this.
//
// Proposal: If flag is being added in the middle of some text, then expand the
// text by adding it inline.  But if flag is being added at the end (last
// child) of a layout element, then instead float it above the element to avoid
// changing the size of the container.  This will require determination of the
// real offset of the element.  Note the link may be inside a <b> which we must
// step outside to determine whether it is at the end of a container or not.



// == Config == //

var max_width = 300;
var getWebsiteInfoOnly = false;   // Shows info for target website, not target page.
var bg_color = "#EAEAEA";
var warn_color = "#FF4444";
var border_color = "#AAAAAA";
var font_color = "#000000";
var font_face = ""; // "tahoma";
var font_size = "11px";



// == Library functions == //

function hsv2rgbString(h,s,v) {
	// hsv input values range 0 - 1, rgb output values range 0 - 255
	// Adapted from http://www.easyrgb.com/math.html
	var red, green, blue;
	if(s == 0) {
		red = green = blue = Math.round(v*255);
	} else {
		// h should be < 1
		var var_h = h * 6;
		if (var_h == 6) var_h = 0; // TODO: get offset if h<0 or h>1
		var var_i = Math.floor( var_h );
		var var_1 = v*(1-s);
		var var_2 = v*(1-s*(var_h-var_i));
		var var_3 = v*(1-s*(1-(var_h-var_i)));
		if (var_i==0) {
			red = v; 
			green = var_3; 
			blue = var_1;
		} else if(var_i==1) {
			red = var_2;
			green = v;
			blue = var_1;
		} else if(var_i==2) {
			red = var_1;
			green = v;
			blue = var_3
		} else if(var_i==3) {
			red = var_1;
			green = var_2;
			blue = v;
		} else if (var_i==4) {
			red = var_3;
			green = var_1;
			blue = v;
		} else {
			red = v;
			green = var_1;
			blue = var_2
		}
		red = Math.round(red * 255);
		green = Math.round(green * 255);
		blue = Math.round(blue * 255);
	}
	return "rgb("+red+","+green+","+blue+")";
}

function getHash(str) {
	var sum = 0;
	for (var i=0;i<str.length;i++) {
		sum = sum + (253*i)*(str.charCodeAt(i));
		sum = sum % 100000;
	}
	return sum;
}

function startsWith(bigStr,smallStr) {
	return (smallStr === (""+bigStr).substring(0,smallStr.length));
}

function unescapeHTML(htmlString) {
	var tmpDiv = document.createElement("DIV");
	tmpDiv.innerHTML = htmlString;
	return tmpDiv.textContent;
}

function getCanonicalUrl(url) {
	if (url.substring(0,1)=="/") {
		url = document.location.protocol + "://" + document.location.domain + "/" + url;
	}
	if (!url.match("://")) {
		// Surely wrong: url = document.location.path + "/" + url;
		// Also fail imo: url = document.location.protocol + "://" + document.location.domain + document.location.pathname + "/" + url;
		url = document.location.href.match("^[^?]*/") + url;
	}
	return url;
}

function getHostnameOfUrl(url) {
	return url.split('/')[2];
	// return url.match(/[^:]*:[/][/][^/]*[/]/)[0];
}

function addCommasToNumber(num) {
	var str = (num+"").split(".");
	dec=str[1]||"";
	num=str[0].replace(/(\d)(?=(\d{3})+\b)/g,"$1,");
	return (dec) ? num+'.'+dec : num;
}

function boldTextElement(txt) {
	var span = document.createElement("SPAN");
	try {
		span.appendChild(document.createTextNode(txt));
	} catch (e) {
		log("Problem rendering "+typeof txt+" to textnode: "+txt);
	}
	span.style.fontWeight = 'bold';
	span.style.fontSize = '1.1em';
	return span;
}

function trimString(str) {
	return str.replace(/(^\s*|\s*$)/g,'');
}

// log( "trim1", '' === trimString('   a b c    ') );

// Not checking the element we were passed, but all its children
// BUG: Intended to detect 1 image, but will allow N images.
function isImageAndWhitespace(elem) {
	for (var i=0;i<elem.childNodes.length;i++) {
		var b = elem.childNodes[i];
		var isImage = (b.tagName == "IMG");
		var isEmptyText = (b.nodeType==3 && trimString(b.textContent)=="");
		if (!isImage && !isEmptyText) {
			return false;
		}
	}
	return true;
}



// == Chrome and Bookmarklet compatibility layer == //

if (typeof GM_log == 'undefined') {
	GM_log = function(data) {
		if (this.console && console.log) {
			console.log(data);
		}
		window.status = ""+data;
	};
}

// We always replace Google Chrome's GM_xmlhttpRequest because it is not cross-site.
// Likewise, we always replace FallbackGMAPI's localDomainOnly implementation.
var needToJSONP = !this.GM_xmlhttpRequest || window.navigator.vendor.match(/Google/) || this.GM_xmlhttpRequest.localDomainOnly;

var allowedToJSONP = ( document.location.protocol === "https:" ? enableJSONPonHTTPS : true );

// allowedToJSONP can be overridden by loading this script twice.
// It was still failing in Chrome even when "load insecure content" was enabled!
if (needToJSONP && (allowedToJSONP || secondRun)) {

	// This performs a direct JSONP request from Delicious, circumventing cross-site issues with GM_xhR and XMLHR.
	// BUG: Chrome complains if this runs while we are on an https page, and more sites are switching to https!
	// OLD BUG: Since Delicious last changed format, they started providing responses with e.g. ... "total_posts": 2L, ...  This 'L' does not parse in Javascript!  It throws "identifier starts immediately after numeric literal" in Firefox, and "Unexpected token ILLEGAL" in Chromium.
	// OLD BUG: Interestingly, the "L" does NOT appear if we make a non-callback request through the xhrasjson proxy, or indeed the same callback request direct in Firefox's location bar.  Perhaps we can avoid the "L" by requesting the right content/mime-type in the script tag?
	// Jan 2012 - Has the L gone away?  Script seems to be working fine now!  Perhaps Delicious changed their format back.
	// GM_log("[DLT] Using Delicious JSONP");
	GM_xmlhttpRequest = function(details) {
		// Insert a callback into the root window, anonymised by a random key.
		var callbackName = "dlt_jsonp_callback_" + parseInt(Math.random()*987654321);
		// Where should we place the callback?  Chrome Userscripts need to obtain unsafeWindow.
		var target = window;
		var weAreInUserscriptScope = (typeof GM_log != 'undefined');
		if (window.navigator.vendor.match(/Google/) && weAreInUserscriptScope) {
			var div = document.createElement("div");
			div.setAttribute("onclick", "return window;");
			unsafeWindow = div.onclick();
			target = unsafeWindow;
		}
		var script = document.createElement("script");
		var callbackFunction = function(dataObj) {
			var responseDetails = {};
			responseDetails.responseText = JSON.stringify(dataObj);
			try {
				details.onload(responseDetails);
			} catch (e) {
				// GM_log("[DLT] Problem running details.onload: "+e);
				GM_log("[DLT] Problem running details.onload ("+details.onload+"): "+e);
			}
			// Cleanup artifacts: script and callback function
			delete target[callbackName];
			// document.getElementsByTagName("head")[0].removeChild(script);
			document.body.removeChild(script);
		};
		target[callbackName] = callbackFunction;
		// Request a JSONP response from delicious, which should return some javascript to call the callback.
		script.type = "text/javascript";
		script.src = details.url + "?callback="+callbackName;
		// GM_log("[DLT] Requesting script "+script.src);
		// GM_log("[DLT] Adding "+script+" to "+document.body);
		// document.getElementsByTagName("head")[0].appendChild(script);
		document.body.appendChild(script);
	};

	/*
	//// GM_xhR alternative through a JSONP Proxy
	//// Get your proxy here (for node.js): http://hwi.ath.cx/javascript/xhr_via_json/
	GM_log("[DLT] Attempting to use GM_xhr JSONP proxy ...");
	GM_xmlhttpRequest = function(details) {
		var proxyHost = "hwi.ath.cx:8124";
		// We don't want to send functions to the proxy, so we remove them from the details object.
		var onloadCallback = details.onload;
		var onerrorCallback = details.onerror;
		var onreadystatechangeCallback = details.onreadystatechange;
		delete details.onload;
		delete details.onerror;
		delete details.onreadystatechange;
		// Insert a callback into the root window, anonymised by a random key.
		var callbackName = "xss_xhr_via_jsonp_callback_" + parseInt(Math.random()*987654321);
		var callbackFunction = function(responseDetails) {
			if (onreadystatechangeCallback) {
				responseDetails.readyState = 4;
				onreadystatechangeCallback(responseDetails);
			}
			if (onloadCallback) {
				onloadCallback(responseDetails);
			}
		};
		var weAreInUserscriptScope = (typeof GM_log != 'undefined');
		if (!window.navigator.vendor.match(/Google/) || !weAreInUserscriptScope) {
			// This works fine in Firefox GM, or in Chrome's content scope.
			window[callbackName] = callbackFunction;
		} else {
			// But the window seen from Chrome's userscript scope is sandboxed,
			// and many updates are not shared between scopes.
			// So we must get Chrome's unsafeWindow (the real content window).
			var div = document.createElement("div");
			div.setAttribute("onclick", "return window;");
			unsafeWindow = div.onclick();
			// And place the callback in that.
			unsafeWindow[callbackName] = callbackFunction;
		}
		// Request an XHR response from the proxy, which should return some javascript to call the callback.
		var reqStrung = JSON.stringify(details);
		var params = "details="+encodeURIComponent(reqStrung)+"&callback="+callbackName;
		var script = document.createElement("script");
		script.type = "text/javascript";
		script.src = "http://" + proxyHost + "/xhrasjson?" + params;
		document.getElementsByTagName("head")[0].appendChild(script);
		// The callback should run on a successful response.  But we need to handle errors too.
		// script.onload = function(e) { GM_log("[DLT] Script has loaded."); };
		script.onerror = function(e) {
			var responseDetails = {};
			responseDetails.status = 12345;
			if (onreadystatechangeCallback) {
				responseDetails.readyState = 4;
				onreadystatechangeCallback(responseDetails); // This gets called even on error, right?
			}
			if (onerrorCallback) {
				onerrorCallback(responseDetails);
			}
			throw new Error("Failed to get JSONed XHR response from "+proxyHost+" - the server may be down.");
		};
	};
	*/

} else if (needToJSONP && !allowedToJSONP) {
	GM_log("[DLT] Not attempting to Delicious since we are on https page.");
	// We must not use this return; in Firefox.  It causes a "SyntaxError: return not in function" to be thrown, without running even the top of this script.
	// return;
	// Instead we throw an error:
	throw new Error("[DLT] Not attempting to Delicious since we are on https page.");
}

if (typeof GM_addStyle == 'undefined') {
	GM_addStyle = function(css) {
		var style = document.createElement('style');
		style.textContent = css;
		document.getElementsByTagName('head')[0].appendChild(style);
	};
}

// DONE: It would be more appropriate to use globalStorage if it is available.
// TODO: But then to be strictly GM-compatible, it should also separate keys by script namespace and name and number!

// This throws an error in FF4 GM: GM_setValue.toString().indexOf("not supported")>=0

if (typeof GM_setValue === 'undefined' || window.navigator.vendor.match(/Google/)) {

	/*
	var storage = this.globalStorage || this.localStorage || this.sessionStorage;
	var name = "some unlabeled";
	*/

	// Removed "globalStorage" storage because in FF4 it offers it but errors when we try to use it.
	// Chrome doesn't offer it ATM.
	var storageTypes = ["localStorage","sessionStorage"];
	var storage,name;
	for (var i=0;i<storageTypes.length;i++) {
		if (this[storageTypes[i]]) {
			storage = this[storageTypes[i]];
			name = storageTypes[i];
			break;
		}
	}

	if (storage) {
		try {
			storage.length; storage.getItem("dummy");
		} catch (e) {
			alert("Tried to use "+name+" but it failed on us!");
			storage = null;
		}
	}

	if (storage) {

		/*
		var allStorages = [this.globalStorage,this.localStorage,this.sessionStorage];
		var names = ["Global","Local","Session"];
		var i = (allStorages.indexOf ? allStorages.indexOf(storage) : 0);
		var name = (i >= 0 ? names[i] : "unknown");
		*/

		GM_log("[DLT] Implementing GM_get/setValue using "+name+" storage.");

		GM_setValue = function(name, value) {
			value = (typeof value)[0] + value;
			storage.setItem(name, value);
		};

		GM_getValue = function(name, defaultValue) {
			var value = storage.getItem(name);
			// GM_log("[DLT] GM_get("+name+")");
			// GM_log("[DLT]   gave: "+value);
			if (!value)
				return defaultValue;
			var type = value[0];
			value = value.substring(1);
			switch (type) {
				case 'b':
					return value == 'true';
				case 'n':
					return Number(value);
				default:
					return value;
			}
		};

		GM_deleteValue = function(name) {
			storage.removeItem(name);
		};

		GM_listValues = function() {
			var list = [];
			for (var i=0;i<storage.length;i++) {
				var key = storage.key(i);
				list.push(key);
				// CUSTOM: Only list those values relevant to this plugin!
				// This is not needed since we do it later anyway, in case we have
				// a dodgy implementation of GM_listValues from elsewhere!
				// if (startsWith(key,"CachedResponse")) {
					// list.push(key);
				// }
			}
			GM_log("[DLT] localstorage is holding "+storage.length+" records, "+list.length+" of which are DLT cached responses.");
			return list;
		};

		// Reduce cache size since with localStorage we will have many
		// site-specific caches, not just one!
		if (!document.location.host.match(/wikipedia/)) {
			maxCacheSize = maxCacheSizeForLocalStorage; // per domain
		} else {
			// Wikipedia has many links, so we won't reduce his cachesize.
			// In fact we will make an exception for wikipedia, and double the cache size!
			maxCacheSize *= 2;
			// This assumes localStorage is more capable than Greasemonkey's data
			// store (otherwise we should increase GM's max), or we are accepting
			// the cleanup overhead for the benefit of a large cache on Wikipedia.
		}

	} else {
		GM_log("[DLT] Warning: Could not implement GM_get/setValue.");
		GM_setValue = function(){};
		GM_getValue = function(key,def){ return def; };
		GM_deleteValue = function(){};
		GM_listValues = function() { return []; };
	}

}

// I don't know a need for this at the moment.  My browsers either give me
// get/set/list or nothing at all.
// If we are adding this into a userscript after data already exists, we might
// find some of the existing names through GM_getValue.
// TODO: UNTESTED!
if (typeof GM_listValues == 'undefined') {
	GM_log("[DLT] Implementing GM_listValues using intercepts.");
	var original_GM_setValue = GM_setValue;
	var original_GM_deleteValue = GM_deleteValue;
	GM_setValue = function(name, value) {
		original_GM_setValue(name,value);
		var values = JSON.parse(GM_getValue("#_LISTVALUES") || "{}");
		values[name] = true;
		original_GM_setValue("#_LISTVALUES",JSON.stringify(values));
	};
	GM_deleteValue = function(name) {
		original_GM_deleteValue(name);
		var values = JSON.parse(GM_getValue("#_LISTVALUES") || "{}");
		delete values[name];
		original_GM_setValue("#_LISTVALUES",JSON.stringify(values));
	};
	GM_listValues = function() {
		var values = JSON.parse(GM_getValue("#_LISTVALUES") || "{}");
		var list = [];
		for (var key in values) {
			list.push(key);
		}
		GM_log("[DLT] GM_listValues is holding "+list.length+" records.");
		return list;
	};
}

// Not everyone has JSON!  Here is a cheap and insecure fallback.
if (!this.JSON) {
	GM_log("[DLT] Implementing JSON using uneval/eval (insecure!).");
	this.JSON = {
		parse: function(str) {
			return eval(str);
		},
		stringify: function(obj) {
			return uneval(str);
		},
	};
}

/*
// Chrome also has no uneval, which forced me to change all eval/unevals to
// JSON.parse/stringify, which was a good thing.  :)
// Problem is, my stored data in Firefox was in uneval format, not JSON...
// This wrapper ensures we can parse both types of stored string (new JSON, old uneval).
function JSON_parse(str) {
	var result;
	try {
		result = JSON.parse(str);
	} catch (e) {
		// Parse Error?  Must be an old record!
		GM_log("[DLT] Difficulty parsing \""+str+"\" with JSON - trying uneval.  e="+e);
		try {
			result = eval(str);
		} catch (e2) {
			GM_log("[DLT] Could not parse: \""+str+"\".  e2="+e2);
			result = null;
		}
	}
	return result;
}
*/
// DONE: drop this a couple of month into 2011.  if i haven't fixed those old records by then, I never will!  (If I do hit one again, the error will be thrown again.)
function JSON_parse(str) {
	return JSON.parse(str);
}



// == Delicious lookup service, and cache == //

var dataCache = ({});

var delay = 1000; // slow down the lookupSpeed if we are making many queries

function doLookup(lookupURL,onSuccess,onFailFn) {
	// lookupSpeed = 3000 + 5000*Math.random(); // Do not poll Delicious again for 3-8 seconds
	// lookupSpeed = 1200 + delay;
	// if (delay < 9000)
		// delay += 120;
	delay = delay * 1.08;
	lookupSpeed = delay;
	if (logRequests) {
		log("Requesting info for: "+lookupURL);
	}

	// We can use https here, but it is slower.
	// var jsonUrl = 'http://feeds.delicious.com/v2/json/urlinfo?url=' + encodeURIComponent(lookupURL);
	//// In 2010 this feed URL changed, requiring "?callback" rather than "&callback"
	// In Chrome using https gave an error.  In Firefox (real GM_xmlhttpRequest), Delicious did not respond!
	// Conclusion: keep using http, and click "load insecure content" in Chrome.  :)
	var s = document.location.protocol == "https:" ? "s" : "";
	// var s = "";
	var jsonUrl = 'http'+s+'://feeds.delicious.com/v2/json/urlinfo/' + hex_md5(lookupURL);
	GM_xmlhttpRequest({
		method: "GET",
		url: jsonUrl,
		headers: {
			"Accept":"text/json"
		},
		onload: responseHandler
	});

	function responseHandler(response) {

		if (logResponses) {
			log(("Delicious responded: "+response.responseText+" to "+lookupURL).substring(0,180));
		}
		var resultObj;
		try {
			resultObj = JSON.parse(response.responseText); // older browsers might need to @require json2.js
		} catch (e) {
			log("Failed to parse responseText with JSON: "+response.responseText);
			// resultObj = eval(response.responseText); // INSECURE!
		}
		// The data we want is usually the first element in the array
		if (resultObj && resultObj[0] && resultObj[0].total_posts) {
			resultObj = resultObj[0];
		}
		// We should save a record, even if Delicious had no info, so we
		// won't request a lookup on the same URL.
		// Sometimes Delicious returns "[]" which gets evaled as an Object but
		// one we cannot write to (uneval does not reflect our changes).
		if (resultObj==null || JSON.stringify(resultObj)=="[]") {
			resultObj = {};
		}
		// Overwrite "working..." with the good or failed result
		dataCache[lookupURL] = resultObj;
		dataCache[lookupURL].cacheCount = 0;
		dataCache[lookupURL].lastUsed = new Date().getTime();
		GM_setValue("CachedResponse"+lookupURL,JSON.stringify(dataCache[lookupURL]));
		// log("Set data: "+GM_getValue("CachedResponse"+lookupURL));
		if (resultObj.total_posts) {
			onSuccess(resultObj,lookupURL);
		} else {
			if (onFailFn) {
				onFailFn();
			} else {
				GM_log("[DLT] No result from Delicious, and no onFailFn.");
			}
		}

	}

	// TODO: The GM_xmlhttpRequest should also include an
	// onreadystatechange function to catch any failed requests.
}

function isEmptyObject(o) {
	for (var prop in o) {
		if (prop=="cacheCount" || prop=="lastUsed")
			continue; // do not count as counted properties!
		return false;
	}
	return true;
}

function tryLookup(subjectUrl,onSuccess,onFailure) {
	// Is this record already in our runtime cache?
	if (dataCache[subjectUrl] == null) {
		// If not, try to load it from our persistent cache.
		dataCache[subjectUrl] = JSON_parse(GM_getValue("CachedResponse"+subjectUrl,"null"));
	}
	if (dataCache[subjectUrl] != null) {
		// Having an empty record in the cache is considered a "failure" (to allow for double lookups)
		if (isEmptyObject(dataCache[subjectUrl])) {
			onFailure(dataCache[subjectUrl],subjectUrl);
			return;
		}
		dataCache[subjectUrl].cacheCount++;
		dataCache[subjectUrl].lastUsed = new Date().getTime();
		// I'm paranoid that setValue takes time to complete!  I guess we could just put it after.
		setTimeout(function(){
			GM_setValue("CachedResponse"+subjectUrl,JSON.stringify(dataCache[subjectUrl]));
		},20);
		onSuccess(dataCache[subjectUrl],subjectUrl);
	} else {
		dataCache[subjectUrl] = "working...";
		doLookup(subjectUrl,onSuccess,onFailure);
	}
}

function age(cachedRecord) {
	if (!cachedRecord || cachedRecord.lastUsed == null) {
		// log("Error: record has no date! "+uneval(cachedRecord));
		return 30;
	} else {
		var ageInMilliseconds = (new Date().getTime() - cachedRecord.lastUsed);
		// var ageInMonths = ageInMilliseconds / 86400000 / 30;
		var ageInDays = ageInMilliseconds / 86400000;
		// Avoid division by zero on current time, and future times
		if (ageInDays <= 0.01)
			ageInDays = 0.01;
		return ageInDays;
	}
}

function cleanupCache() {
	if (typeof GM_listValues === 'undefined') {
		log("Cannot cleanupCache - GM_listValues() is unavaiable.");
	} else {

		if (displayCleanupInTitle) {
			var oldTitle = ""+document.title;
			document.title = "[Cleaning cache..]";
		}

		log("Starting cleanupCache ...");
		var startTime = new Date().getTime();

		var fullCacheList = GM_listValues();
		if (typeof fullCacheList.length != 'number') {
			log("Unable to cleanup cache, since fullCacheList has no length!");
			log("fullCacheList =",fullCacheList);
		}

		var realMaxThreshold = maxCacheSize*1.10;

		if (fullCacheList.length < realMaxThreshold) {
			// Before we even trim, if we are below the thresold, we know we won't cleanup.
			// So we can just skip it all!
			fullCacheList = [];
		}

		// Trim the list down to only those entries relevant to this plugin.
		// (Slow but neccessary when we are using localStorage.)
		var cacheList = [];
		for (var i=0;i<fullCacheList.length;i++) {
			var crURL = fullCacheList[i];
			if (startsWith(crURL,"CachedResponse")) {
				cacheList.push(crURL);
			}
		}
		fullCacheList = null; // So GC can act soon if it wishes

		/*
		cacheList = fullCacheList;   // Let's hope it's a real sortable Array!
		*/

		if (cacheList.length < realMaxThreshold) {

			log("Skipping cleanup until overflowed by 10%.");
			// The process below is damn heavy.
			// So we don't want to do it to clean up only 5 records!
			// maxCacheSize should more accurately be called minCacheSize ;)

		} else {

			log("There are "+cacheList.length+" items in the cache.");

			if (displayCleanupInTitle) {
				document.title = "[Cleaning cache...]";
			}

			// Rather casual method: Keep deleting records at random until we meet
			// our max cache size.
			/*
			while (cacheList.length > 512) {
				var i = parseInt(Math.random() * cacheList.length);
				// log("Deleting "+cacheList[i]+" length is currently "+cacheList.length);
				GM_deleteValue(cacheList[i]);
				// delete cacheList[i];
				cacheList[i] = cacheList[cacheList.length-1];
				cacheList.length--;
			}
			*/

			function getScoreFor(crURL) {
				var url = crURL.replace(/^CachedResponse/,'');
				if (dataCache[url] == null) {
					dataCache[url] = JSON_parse(GM_getValue(crURL,"null"));
				}
				if (dataCache[url] == null) {
					log("Warning! Empty cache entry for "+url);
					dataCache[url] = {};
				}
				// All cached urls have a minimum score of 2.
				// log("Generating score form cacheCount="+dataCache[url].cacheCount+" and age="+age(dataCache[url]));
				var thisScore = (dataCache[url].cacheCount+2) / age(dataCache[url]);
				if (isNaN(thisScore)) // e.g. if dataCache[url] == null
					thisScore = 0.00001;
				return thisScore;
			}

			// Inefficient method: find the least valuable entry, remove it, then
			// repeat.
			/*
			// Delete the oldest and least-used, keep new/popular entries
			function cleanupOne() {
				if (cacheList.length > maxCacheSize) {
					var poorestScore = 99999;
					var poorestScorer = null;
					for (var i=0;i<cacheList.length;i++) {
						if (Math.random() < 0.25) // Only really scan quarter the record set
							continue; // so we sometimes keep new (cacheCount=0) records
						var crURL = cacheList[i]; // e.g. "CachedResponsehttp://..."
						thisScore = getScoreFor(crURL);
						if (!poorestScorer || thisScore < poorestScore) {
							poorestScore = thisScore;
							poorestScorer = crURL;
							poorestScorerIndex = i;
						}
					}
					if (poorestScorer == null) {
						log("Found nothing to cleanup.");
						return false;
					}
					var url = poorestScorer.replace(/^CachedResponse/,'');
					// log("Cleaning up "+poorestScorer+" with "+ ( dataCache[url] ? "age="+age(dataCache[url])+" and count="+dataCache[url].cacheCount : "score="+poorestScore ) +" and uneval="+uneval(dataCache[url]) );
					log("Cleaning up "+poorestScorer+" with count="+dataCache[url].cacheCount+" and age="+(""+age(dataCache[url])).substring(0,6));
					GM_deleteValue(poorestScorer);
					// cacheList = GM_listValues();
					cacheList[poorestScorerIndex] = cacheList[cacheList.length-1];
					cacheList.length--;

					setTimeout(cleanupOne,100);
				} else {
					if (displayCleanupInTitle) {
						if (oldTitle === "")
							oldTitle = " ";   // If we try to set "", Chrome does nothing
						document.title = oldTitle;
					}
					log("Cleanup took "+(new Date().getTime() - startTime)/1000+" seconds.");
				}
			}

			setTimeout(cleanupOne,100);
			*/

			// Better scalable method: Find all the low scorers in one pass:
			// Disadvantage: not run on a timer!

			if (displayCleanupInTitle) {
				document.title = "[Cleaning cache....]";
			}

			function compare(crURLA, crURLB) {
				return getScoreFor(crURLA) - getScoreFor(crURLB);
			}

			var sortedList = cacheList.sort(compare);

			if (displayCleanupInTitle) {
				document.title = "[Cleaning cache....]";
			}

			/*
			GM_log("[DLT] Score for 0 = "+getScoreFor(sortedList[0]));
			GM_log("[DLT] Score for 10 = "+getScoreFor(sortedList[10]));
			GM_log("[DLT] Score for 100 = "+getScoreFor(sortedList[100]));
			*/

			if (sortedList.length > maxCacheSize) {
				GM_log("[DLT] Removing "+(sortedList.length-maxCacheSize)+" records.");
			}

			// sortedList.slice(maxCacheSize)
			for (var i=maxCacheSize; i<sortedList.length; i++) {
				var crURL = sortedList[i]; // e.g. "CachedResponsehttp://..."
				var poorestScorer = crURL;
				var url = crURL.replace(/^CachedResponse/,'');
				// log("Cleaning up "+poorestScorer+" with count="+dataCache[url].cacheCount+" and age="+(""+age(dataCache[url])).substring(0,6));
				GM_deleteValue(poorestScorer);
			}

			log("Cleanup took "+(new Date().getTime() - startTime)/1000+" seconds.");

		}

		if (displayCleanupInTitle) {
			if (oldTitle === "")
				oldTitle = document.location.href;   // If we set "" or " ", Chrome does nothing!
			document.title = oldTitle;
		}

	}
}





// == Link tooltip (popup on rollover) == //

var timer;
var stillFocused;
var tooltipDiv;

var rolledOverTooltip = false;

// A different version of tryLookup, which will try to lookup the hostname if the initial URL failed.
function initiateDoubleLookup(lookupURL,onSuccess) {
	var onFailure;
	var hostUrl = "http://"+getHostnameOfUrl(lookupURL)+"/";
	if (hostUrl != lookupURL) {
		onFailure = function() {
			lookupURL = hostUrl;
			tryLookup(lookupURL,onSuccess,onSuccess);
			// ,function(){log("Both URL and host lookup failed for "+hostUrl+"");});
		};
	} else {
		onFailure = onSuccess;
	}
	tryLookup(lookupURL,onSuccess,onFailure);
}

function positionTooltip(evt) {
	if (tooltipDiv) {
		var posx = evt.clientX + window.pageXOffset;
		var posy = evt.clientY + window.pageYOffset;

		tooltipDiv.style.right = '';
		tooltipDiv.style.left = (posx + 15) + "px";
		tooltipDiv.style.top = (posy + 12) + "px";

		if (tooltipDiv.clientWidth > max_width) {
			tooltipDiv.style.width = max_width + "px";
		}

		var scrollbarWidth = 20;
		// If the cursor is so low in the window that the tooltip would appear
		// off the bottom, then we move it to above the cursor
		if (parseInt(tooltipDiv.style.top) + tooltipDiv.clientHeight + scrollbarWidth > window.innerHeight + window.pageYOffset) {
			tooltipDiv.style.top = (posy - 7 - tooltipDiv.clientHeight) + "px";
		}
		// Similar for the right-hand-side, but beware the browser might have
		// reduced clientWidth in an attempt to make everything fit, so instead
		// we use divWidth.
		var divWidth = ( tooltipDiv.clientWidth > max_width ? tooltipDiv.clientWidth : max_width ) + 20;
		if (parseInt(tooltipDiv.style.left) + divWidth + scrollbarWidth > window.innerWidth + window.pageXOffset) {
			// tooltipDiv.style.left = (posx - 15 - divWidth) + "px";
			tooltipDiv.style.left = '';
			tooltipDiv.style.right = (window.innerWidth - posx + 5) + "px";
			// tooltipDiv.style.width = divWidth;
			if (tooltipDiv.clientWidth > max_width) {
				tooltipDiv.style.width = max_width + "px";
			}
			// TODO: Can move the tooltip too far left, if it is naturally narrow.
		}
		// The +scrollbarWidth deals with the case when there is a scrollbar
		// which we don't want to be hidden behind!
		// TODO: To prevent the flashing of scrollbar (visible in Konq, invisible
		// but slow in FF), we should use desiredWidth/Height to check the
		// situation before setting bad left/top in the first place.
		// This does still occasionally occur, but seems to go away after the
		// first move-to-left.
	}
}

function showResultsTooltip(resultObj,subjectUrl,evt) {
	// if (stillFocused == link) {
	// log("Displaying results for "+subjectUrl+": "+uneval(resultObj));
	hideTooltip();

	tooltipDiv = document.createElement("div");
	tooltipDiv.id = "DLTtooltip";
	// tooltipDiv.setAttribute("style", "background:" + bg_color + ";border:1px solid " + border_color + ";padding:2px;color:" + font_color + ";font-family:" + font_face + ";font-size:" + font_size + ";position:absolute;z-index:100000;")
	tooltipDiv.style.backgroundColor = bg_color;
	tooltipDiv.style.border = "1px solid "+border_color;
	tooltipDiv.style.padding = '6px';
	tooltipDiv.style.color = font_color;
	tooltipDiv.style.fontFamily = font_face;
	tooltipDiv.style.fontSize = font_size;
	tooltipDiv.style.position = "absolute";
	tooltipDiv.style.zIndex = 100000;
	tooltipDiv.style.textAlign = 'left';
	tooltipDiv.style.lineHeight = '';

	tooltipDiv.addEventListener('mouseover',function(evt){
			rolledOverTooltip = true;
	},false);

	tooltipDiv.addEventListener('mouseout',function(evt){
			hideTooltipMomentarily();
	},false);

	// Sometimes Delicious returns a result with nothing but the hash and
	// total_posts=1.  These days I am getting this more often than
	// resultObj==null, but it is about as informative, so:
	if (resultObj && resultObj.total_posts==1 /*&& resultObj.hash*/ &&
			resultObj.url=="" && resultObj.title=="" &&
			resultObj.top_tags.length==0) {
		GM_log("[DLT] Got boring response: "+JSON.stringify(resultObj));
		resultObj = null;
	}
	// However I think this may be due to the fact that there are *private*
	// bookmarks for the URL.  If so, Delicious is a little bit naughty in
	// admitting that they even exist in its database!
	// TODO: Test this theory!

	if (resultObj && resultObj.total_posts) {

		if (unescapeHTML(resultObj.url) != subjectUrl) {
			var warningSpan = document.createElement("SPAN");
			if (unescapeHTML(resultObj.url) == getHostnameOfUrl(subjectUrl)) {
				warningSpan.appendChild(document.createTextNode("Results for website"));
			} else {
				// Sometimes Delicious returns a different URL from the one requested.
				// So far this has always seemed fine to me, either adding or
				// removing index.html, or changing a few irrelevant CGI params.
				// TODO/CONSIDER: So maybe the warning colors/message are not needed?
				// Hmm no there is an example where it's not fine.  The "Shopping"
				// link along the top of Google's search results page.  It's
				// actually just a bunch of CGI parameters.  But Delicious drops
				// all the parameters and gives me the results for the top domain
				// page (http://www.google.com/).  This is not the link I wanted
				// info for, so the warnings are needed!
				warningSpan.appendChild(document.createTextNode("Results for: "+resultObj.url));
			}
			warningSpan.style.backgroundColor = warn_color; // TODO: Oddly this does not get applied in Konqueror!
			warningSpan.style.color = 'white';
			warningSpan.style.padding = '3px 6px';
			warningSpan.style.fontWeight = 'bold';
			tooltipDiv.appendChild(warningSpan);
			tooltipDiv.appendChild(document.createElement('BR'));
		}

		var titleCont = document.createElement("TD");
		titleCont.style.backgroundColor = bg_color;

		// titleCont.appendChild(document.createTextNode("Title: "));
		// titleCont.appendChild(boldTextElement(resultObj.title));
		// titleCont.appendChild(document.createElement('BR'));
		//// The title can contain HTML-encoded chars, so we must decode/present accordingly
		// titleCont.innerHTML += "<B style='font-size:1.1em;'>" + resultObj.title + "</B><BR>";

		var titleElem = boldTextElement(unescapeHTML(resultObj.title));

		// Make it a link?
		var link = document.createElement("A");
		link.href = 'http://delicious.com/url/view?url='+encodeURIComponent(resultObj.url)+'&show=notes_only';
		link.target = "_blank";
		link.style.paddingRight = '8px';
		link.appendChild(titleElem);
		titleElem = link;

		titleCont.appendChild(titleElem);
		//// For some reason Firefox refuses to notice the addition of this
		//// style, so we do it with a CSS class instead.
		// titleCont.className = 'dlttLeft';

		// titleCont.appendChild(document.createTextNode("Popularity: "));
		// titleCont.appendChild(boldTextElement(""+resultObj.total_posts));
		// var popWidth = Math.log(3 + parseInt(resultObj.total_posts))*30;
		var popWidth = Math.log(Number(resultObj.total_posts)/40)*max_width/8;
		if (!popWidth || popWidth<=10) popWidth = 10;
		if (popWidth>max_width) popWidth = max_width;
		var popBar = document.createElement('A');
		popBar.href = link.href;

		popBar.style.backgroundColor = getColorForCount(resultObj.total_posts);
		popBar.style.color = 'white';
		popBar.style.width = popWidth+'px';
		popBar.style.margin = '2px 0px 2px 0px';
		popBar.style.padding = '3px 8px 2px 8px';
		popBar.style.textAlign = 'right';
		popBar.style.textDecoration = 'none'; // do not underline popularity (although it is a link)
		// popBar.appendChild(document.createTextNode(" "));
		// popBar.style.float = 'right'; //// Ahhh apparently .float was renamed .cssFloat or .styleFloat for IE
		popBar.appendChild(boldTextElement(addCommasToNumber(resultObj.total_posts)));

		var popBarCont = document.createElement("TD");
		popBarCont.appendChild(popBar);
		// popBarCont.style.float = 'right';
		// popBarCont.className = 'dlttRight';
		// popBarCont.style.position = 'fixed';
		// popBarCont.style.right = '0px';
		popBarCont.style.textAlign = 'right';
		// popBarCont.align = 'right';
		popBarCont.align = 'right';

		// titleCont.appendChild(popBarCont);

		var topTable = document.createElement("TABLE");
		topTable.width = '100%';
		var topRow = document.createElement("TR");
		topTable.appendChild(topRow);
		topRow.appendChild(titleCont);
		topRow.appendChild(popBarCont);
		tooltipDiv.appendChild(topTable);
		topTable.style.backgroundColor = bg_color;
		/*
		titleCont.style.float = 'left';
		popBarCont.style.float = 'right';
		tooltipDiv.appendChild(titleCont);
		tooltipDiv.appendChild(popBarCont);
		tooltipDiv.style.overflow = 'auto'; // Fix floating problems?
		*/

		/* top_tags is a hashtable, it has no .length */
		if (resultObj.top_tags /* && resultObj.top_tags.length>0 */ ) {

			// tooltipDiv.appendChild(document.createElement("BR"));

			var tagsCont = document.createElement("P");
			tagsCont.style.marginTop = '4px';
			tagsCont.style.marginBottom = '1px';
			// tagsCont.style.float = 'right';
			var tagsDiv = document.createElement("DIV");
			tagsDiv.style.textAlign = 'right';

			/*
			//// Simple list
			var tags = "";
			for (var tag in resultObj.top_tags) {
				tags += (tags==""?"":", ") + tag;
			}
			tagsDiv.appendChild(document.createTextNode("Tags: "+tags+""));
			*/

			//// List with colored tags
			var first = true;
			for (var tag in resultObj.top_tags) {
				if (!first)
					tagsDiv.appendChild(document.createTextNode(", "));
				first = false;

				var tagSpan = document.createElement("SPAN");

				tagSpan.appendChild(document.createTextNode(tag));
				tagSpan.style.color = hsv2rgbString( (getHash(tag)%256)/256, 1.0, 0.5 );

				// Make it a link?
				var link = document.createElement("A");
				link.href = "http://delicious.com/tag/"+tag;
				link.target = "_blank";
				link.title = resultObj.top_tags[tag];
				link.style.textDecoration = 'none'; // do not underline tags
				link.appendChild(tagSpan);
				tagSpan = link;

				tagsCont.appendChild(tagsDiv);

				tagsDiv.appendChild(tagSpan);

			}

			tooltipDiv.appendChild(tagsCont);

		}

	} else {
		tooltipDiv.appendChild(document.createTextNode("No delicious tags for "+subjectUrl));
	}
	document.body.appendChild(tooltipDiv);
	positionTooltip(evt);
	// log("tooltipDiv.innerHTML = "+tooltipDiv.innerHTML);
}

// Old: These functions make use of the scope vars link and subjectUrl.
// Note: These functions make use of the scope var dataCache.

function createTooltip(evt) {

	hideTooltip();

	var link = evt.target;

	lastMoveEvent = evt;

	stillFocused = link;

	// Links we do not want to act on:
	if (link.href.match(/^javascript:/))
		return;
	if (document.location.hostname == link.hostname
			&& document.location.pathname == link.pathname)
		return;   // Either a link to self, or a # link to an anchor in self

	var subjectUrl = getCanonicalUrl(link.href);
	// Remove any #anchor from the url
	subjectUrl = ""+subjectUrl.match(/^[^#]*/);

	if (dataCache[subjectUrl] == "working...") {
		return;  // We can't do anything useful here.  We must wait for the XHR to respond.
	}

	var waitTime = ( dataCache[subjectUrl] != null ? 300 : 1000 );

	timer = setTimeout(function(){
		if (stillFocused==link) {
			initiateDoubleLookup(subjectUrl,function(foundResults,foundUrl) {
				showResultsTooltip(foundResults,subjectUrl,lastMoveEvent || evt);
			});
		}
	},waitTime);


}

function hideTooltipMomentarily() {
	rolledOverTooltip = false;
	stillFocused = null;
	setTimeout(function(){
			if (stillFocused == null && !rolledOverTooltip)
				hideTooltip();
	},200);
}

function hideTooltip() {
	stillFocused = null;
	if (timer) {
		clearTimeout(timer);
		timer = null;
	}
	// TODO: iff we are mousingoff the link, we should delay before closing, to see if the user is mousing onto the tooltip, and if so not hide.
	if (tooltipDiv) {
		if (tooltipDiv.parentNode) {
			tooltipDiv.parentNode.removeChild(tooltipDiv);
		}
		tooltipDiv = null;
	}
	rolledOverTooltip = false;
}

function initialiseTooltipListeners() {

	// GM_addStyle(".dlttLeft { float: left; }");
	// GM_addStyle(".dlttRight { float: right; }");

	/*
	for (var i=0; i<document.links.length; i++) {
		var link = document.links[i];
		link.addEventListener("mouseover",createTooltip,false);
		link.addEventListener("mouseout",hideTooltip,false);
		// TODO: We should only really enable the mousemove/out/down events when we have done a mouseover!
		link.addEventListener("mousemove",positionTooltip,false);
		// If user clicks either button on the link, then we hide it
		link.addEventListener("mousedown",hideTooltip,true);
	}
	*/

	// A better event listener, which will respond to links added to the DOM later.
	// DONE: One problem with this method.  If we mouseover a //A/EM then the event
	// doesn't fire!
	// DONE: I guess we better look up the tree at our parentNodes for the A, and maybe
	// even change/fake the evt.target to point to the A!

	// DONE: New bug with the new global event listener.  Now we can't mouseover
	// the tooltip any more.  :s
	// Maybe the problem here is the As inside the tooltip.  But why wasn't that a
	// problem before? :o
	// Solved with checkParentsForId().

	// TODO: One bug with this method is that when moving in or out of the SPAN
	// inside the A, a mouseout then a mouseover get fired on the A.  This is
	// slightly hard to fix, how to we know whether the mouseout should be fired or
	// not?

	// var linksOnly = function(evt) { return (evt && evt.target && evt.target.tagName == "A"); };

	function checkParentsForId(node,id) {
		while (node) {
			if (node.id == id)
				return true;
			node = node.parentNode;
		}
		return false;
	}

	var linksOnly = function(evt) {
		var node = evt.target;
		// We don't want to act on links inside the tooltip
		if (checkParentsForId(node,"DLTtooltip"))
			return null;
		while (node) {
			// We do not want to count e.g. <A name="..."> links!
			if (node.tagName == "A" && node.href)
				return node;
			node = node.parentNode;
		}
		return node;
	};

	addGlobalConditionalEventListener("mouseover",createTooltip,linksOnly);
	addGlobalConditionalEventListener("mouseout",hideTooltipMomentarily,linksOnly);
	// TODO: We should only really enable the mousemove/out/down events when we have done a mouseover!
	// addGlobalConditionalEventListener("mousemove",positionTooltip,linksOnly);
	addGlobalConditionalEventListener("mousemove",function(evt){if (evt.target == stillFocused) { lastMoveEvent=evt; } },linksOnly);
	// If user clicks either button on the link, then we hide it
	addGlobalConditionalEventListener("mousedown",hideTooltip,linksOnly);

	function addGlobalConditionalEventListener(evType,handlerFn,whereToFireFn) {
		document.body.addEventListener(evType,function(evt){
				// if (conditionFn(evt)) {
				var finalTarget = whereToFireFn(evt);
				if (finalTarget) {
					var fakeEvt = ({});
					// Maybe better to do a for (var prop in evt) to construct fakeEvt?
					// Hmm no that acts weird, only showing properties for evt which I
					// have already read!
					// OK then let's just set the ones we know we need:
					fakeEvt.target = finalTarget;
					fakeEvt.clientX = evt.clientX;
					fakeEvt.clientY = evt.clientY;
					/* if (evType != "mousemove") {
						log("Performing "+evType+" on "+fakeEvt.target);
					} */
					return handlerFn(fakeEvt);
				}
		},true);
	}

}



// == Initialise Tooltips == //

if (showTooltips) {
	initialiseTooltipListeners();
}



// == Initialise current-page auto-lookup == //

function getColorForCount(tot) {

	/*
	//// Colorful version (light cyan to dull dark blue)
	var thru = Math.log(Number(tot)/40)/8;
	// popBar.style.backgroundColor = 'rgb(128,'+parseInt(127+128*thru)+','+parseInt(255-128*thru)+')';
	// var hue = 2/3 - 1/3*thru;   // blue -> cyan -> green
	// var hue = thru/3;        // red -> yellow -> green
	// var saturation = 0.4;
	// var variance = 0.9;
	var hue = 1.8/3;
	var saturation = 0.6+0.3*thru;
	var variance = 0.9-0.4*thru;
	return hsv2rgbString(hue, saturation, variance);
	*/

	//// One-hue version, faint light to strong dark blue.
	var greatness = Math.min(1.0,Math.log(tot) / Math.log(10000));
	var saturation = 40 + 60*greatness;
	var brightness = 70 - 30*greatness;
	// scoreSpan.style.backgroundColor = hsv2rgbString(2/3,0.3+0.5*greatness,0.8);
	return "hsl(240,"+saturation+"%,"+brightness+"%)";

}

function createScoreSpan(resultObj) {
	var scoreSpan = document.createElement("span");
	if (resultObj.total_posts) {
		var text = addCommasToNumber(resultObj.total_posts);
		scoreSpan.appendChild(boldTextElement(text));
		scoreSpan.style.backgroundColor = getColorForCount(resultObj.total_posts);
	}
	scoreSpan.style.color = 'white';
	scoreSpan.style.fontWeight = 'bold';
	if (resultObj.top_tags) {
		var tagArray = [];
		for (var tag in resultObj.top_tags) {
			tagArray.push(tag);
		}
		// Not needed - they are already sorted
		tagArray.sort( function(a,b) {
				return resultObj.top_tags[a] < resultObj.top_tags[b];
		});
		scoreSpan.title = tagArray.join(", ");
		// scoreSpan.title = "Popularity: "+addCommasToNumber(resultObj.total_posts) + " Tags: "+tagArray.join(", ");
		// scoreSpan.title = addCommasToNumber(resultObj.total_posts)+" "+tagArray.join(", ");
	}
	return scoreSpan;
}

if (lookupCurrentPage) {
	try {
		tryLookup(document.location.href,function(resultObj,subjectUrl,evt){
			if (resultObj.total_posts) {
				var lc_div = createScoreSpan(resultObj);
				lc_div.style.position = 'fixed';
				lc_div.style.top = '20px';
				lc_div.style.right = '20px';
				lc_div.style.padding = '4px';
				lc_div.style.fontSize = '2.0em';
				lc_div.style.zIndex = 1209;
				document.body.appendChild(lc_div);
			}
		},function(){
			/* Do nothing on failure. */
		});
	} catch (e) {
		log("DLT On "+document.location);
		log("DLT trying current page lookup, caught exception: "+e);
		log(e);
	}
}



// == Initialise all-links auto-lookup == //

var lookupSpeed = 50;
var lastHref = null;

function addLabel(link) {
	var url = link.href;
	var sameAsLast = (url == lastHref);
	var sameHost = (link.host==document.location.host);
	var badHost = (link.host=="webcache.googleusercontent.com") || (link.host.match(".google.com"));
	var goodUrl = startsWith(url,"http://") || startsWith(url,"https://") || url.indexOf(":")==-1; // skip any "about:config" or "javascript:blah" links
	var hasHash = (url.indexOf("#") >= 0);
	var samePage = (url.split("#")[0] == document.location.href.split("#")[0]);
	// Some links are just too common to spam Deliciouos for all of them.
	// e.g. searches maybe not, but page 3 of the search result no!
	var isGoogleSearch = ( url.indexOf("/search?")>-1 || url.indexOf("/setprefs?")>-1 );
	var isCommonSearch = ( url.indexOf('?q=')>=0 || url.indexOf('&q=')>=0 );
	var isSearch = isCommonSearch || isGoogleSearch;
	if (isCommonSearch) {
		// GM_log("[DLT] Skipping due to ?q= or &q= in "+url);
	}
	var isImage = false; // isImageAndWhitespace(link);
	var isBlacklisted = url.match(/fbcdn.net\//) || url.match(/facebook.com\//);
	var causesDivGrowthOnGoogle = link.parentNode.className=='gbt' || link.className=='gbzt';
	if (sameAsLast || badHost || isSearch || samePage || isImage || isBlacklisted || causesDivGrowthOnGoogle) {
		return;
	}
	if (url && goodUrl) { // && !sameHost

		function addAnnotationLabel(resultObj,subjectUrl,evt){
			// log("Adding annotation for "+url+" with result "+JSON.stringify(resultObj));
			if (resultObj && resultObj.total_posts) {
				var newDiv = createScoreSpan(resultObj);
				var text = " " + (resultObj && addCommasToNumber(resultObj.total_posts)) + " ";
				// BUG TODO: Sometimes the left or right space gets ignored, because we are in a span, and it merges with outer text
				newDiv.textContent = text;
				newDiv.style.marginLeft = '0.2em';
				newDiv.style.marginRight = '0.4em';
				newDiv.style.padding = '0.1em 0.2em 0.1em 0.2em';
				// newDiv.style.padding = '0';
				newDiv.style.verticalAlign = "middle";
				// newDiv.style.verticalAlign = "super";
				// newDiv.style.marginTop = '-1.5em'; // For "super", sometimes works.
				// newDiv.style.marginBottom = '-1.5em'; // Try to not make our parent line grow taller
				newDiv.style.opacity = 0.7;
				// newDiv.style.fontSize = parseInt((greatness+0.1)*10)/10 + 'em';
				newDiv.style.fontSize = '0.6em';
				// newDiv.style.paddingBottom = "0.2em";
				// newDiv.firstChild.style.backgroundColor = "hsv("+2/3+","+greatness+",0.8)";
				link.parentNode.insertBefore(newDiv,link.nextSibling);
				// link.parentNode.appendChild(newDiv);
				// newDiv.addEventListener('mouseover',function(evt){ showResultsTooltip(resultObj,subjectUrl,evt); },false);
				// newDiv.addEventListener('mouseout',hideTooltipMomentarily,false);
			}
			resetColor();
		}

		var resetColor;
		if (showProgress) {
			var oldbgcol = link.style.backgroundColor;
			link.style.backgroundColor = '#ffff44';
			resetColor = function() {
				// log("Resetting color of "+link);
				link.style.backgroundColor = oldbgcol;
			};
		} else {
			resetColor = function() { };
		}

		lastHref = url;

		tryLookup(url,addAnnotationLabel,resetColor);

	}
}

// Let's not auto-annotate links on delicious pages - it is likely to be redundant information!
if (annotateAllLinks && document.location.host != "www.delicious.com") {
	(function(){
		var links = document.getElementsByTagName("A");
		var i = 0;
		function doOne() {
			if (i < links.length) {
				addLabel(links[i]);
				i++;
				if (lookupSpeed <= 50 && Math.random()<0.8) {
					doOne();
				} else {
					setTimeout(doOne,lookupSpeed);
					lookupSpeed = 20;
				}
			} else {
				log("Considered "+i+" links on "+document.location+" for annotation with Delicious labels.");
			}
		}
		doOne();
	})();
}



// == Cleanup old cached data == //

if (Math.random() < 0.1) {
	setTimeout(cleanupCache,1000 * (120+120*Math.random()));
	// 3 minutes +/- 1 minute, to avoid e.g. overlap when opening many pages at the same moment
}



/*
 * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
 * Digest Algorithm, as defined in RFC 1321.
 * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002.
 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
 * Distributed under the BSD License
 * See http://pajhome.org.uk/crypt/md5 for more info.
 */

/*
 * Configurable variables. You may need to tweak these to be compatible with
 * the server-side, but the defaults work in most cases.
 */
var hexcase = 0;  /* hex output format. 0 - lowercase; 1 - uppercase        */
var b64pad  = ""; /* base-64 pad character. "=" for strict RFC compliance   */
var chrsz   = 8;  /* bits per input character. 8 - ASCII; 16 - Unicode      */

/*
 * These are the functions you'll usually want to call
 * They take string arguments and return either hex or base-64 encoded strings
 */
function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));}
function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));}
function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));}
function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); }
function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); }
function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); }

/*
 * Perform a simple self-test to see if the VM is working
 */
function md5_vm_test()
{
  return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72";
}

/*
 * Calculate the MD5 of an array of little-endian words, and a bit length
 */
function core_md5(x, len)
{
  /* append padding */
  x[len >> 5] |= 0x80 << ((len) % 32);
  x[(((len + 64) >>> 9) << 4) + 14] = len;

  var a =  1732584193;
  var b = -271733879;
  var c = -1732584194;
  var d =  271733878;

  for(var i = 0; i < x.length; i += 16)
  {
    var olda = a;
    var oldb = b;
    var oldc = c;
    var oldd = d;

    a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
    d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
    c = md5_ff(c, d, a, b, x[i+ 2], 17,  606105819);
    b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
    a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
    d = md5_ff(d, a, b, c, x[i+ 5], 12,  1200080426);
    c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
    b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
    a = md5_ff(a, b, c, d, x[i+ 8], 7 ,  1770035416);
    d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
    c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
    b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
    a = md5_ff(a, b, c, d, x[i+12], 7 ,  1804603682);
    d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
    c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
    b = md5_ff(b, c, d, a, x[i+15], 22,  1236535329);

    a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
    d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
    c = md5_gg(c, d, a, b, x[i+11], 14,  643717713);
    b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
    a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
    d = md5_gg(d, a, b, c, x[i+10], 9 ,  38016083);
    c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
    b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
    a = md5_gg(a, b, c, d, x[i+ 9], 5 ,  568446438);
    d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
    c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
    b = md5_gg(b, c, d, a, x[i+ 8], 20,  1163531501);
    a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
    d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
    c = md5_gg(c, d, a, b, x[i+ 7], 14,  1735328473);
    b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);

    a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
    d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
    c = md5_hh(c, d, a, b, x[i+11], 16,  1839030562);
    b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
    a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
    d = md5_hh(d, a, b, c, x[i+ 4], 11,  1272893353);
    c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
    b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
    a = md5_hh(a, b, c, d, x[i+13], 4 ,  681279174);
    d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
    c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
    b = md5_hh(b, c, d, a, x[i+ 6], 23,  76029189);
    a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
    d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
    c = md5_hh(c, d, a, b, x[i+15], 16,  530742520);
    b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);

    a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
    d = md5_ii(d, a, b, c, x[i+ 7], 10,  1126891415);
    c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
    b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
    a = md5_ii(a, b, c, d, x[i+12], 6 ,  1700485571);
    d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
    c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
    b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
    a = md5_ii(a, b, c, d, x[i+ 8], 6 ,  1873313359);
    d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
    c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
    b = md5_ii(b, c, d, a, x[i+13], 21,  1309151649);
    a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
    d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
    c = md5_ii(c, d, a, b, x[i+ 2], 15,  718787259);
    b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);

    a = safe_add(a, olda);
    b = safe_add(b, oldb);
    c = safe_add(c, oldc);
    d = safe_add(d, oldd);
  }
  return Array(a, b, c, d);

}

/*
 * These functions implement the four basic operations the algorithm uses.
 */
function md5_cmn(q, a, b, x, s, t)
{
  return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
}
function md5_ff(a, b, c, d, x, s, t)
{
  return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
}
function md5_gg(a, b, c, d, x, s, t)
{
  return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
}
function md5_hh(a, b, c, d, x, s, t)
{
  return md5_cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5_ii(a, b, c, d, x, s, t)
{
  return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
}

/*
 * Calculate the HMAC-MD5, of a key and some data
 */
function core_hmac_md5(key, data)
{
  var bkey = str2binl(key);
  if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz);

  var ipad = Array(16), opad = Array(16);
  for(var i = 0; i < 16; i++)
  {
    ipad[i] = bkey[i] ^ 0x36363636;
    opad[i] = bkey[i] ^ 0x5C5C5C5C;
  }

  var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz);
  return core_md5(opad.concat(hash), 512 + 128);
}

/*
 * Add integers, wrapping at 2^32. This uses 16-bit operations internally
 * to work around bugs in some JS interpreters.
 */
function safe_add(x, y)
{
  var lsw = (x & 0xFFFF) + (y & 0xFFFF);
  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
  return (msw << 16) | (lsw & 0xFFFF);
}

/*
 * Bitwise rotate a 32-bit number to the left.
 */
function bit_rol(num, cnt)
{
  return (num << cnt) | (num >>> (32 - cnt));
}

/*
 * Convert a string to an array of little-endian words
 * If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
 */
function str2binl(str)
{
  var bin = Array();
  var mask = (1 << chrsz) - 1;
  for(var i = 0; i < str.length * chrsz; i += chrsz)
    bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i % 32);
  return bin;
}

/*
 * Convert an array of little-endian words to a string
 */
function binl2str(bin)
{
  var str = "";
  var mask = (1 << chrsz) - 1;
  for(var i = 0; i < bin.length * 32; i += chrsz)
    str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);
  return str;
}

/*
 * Convert an array of little-endian words to a hex string.
 */
function binl2hex(binarray)
{
  var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
  var str = "";
  for(var i = 0; i < binarray.length * 4; i++)
  {
    str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
           hex_tab.charAt((binarray[i>>2] >> ((i%4)*8  )) & 0xF);
  }
  return str;
}

/*
 * Convert an array of little-endian words to a base-64 string
 */
function binl2b64(binarray)
{
  var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  var str = "";
  for(var i = 0; i < binarray.length * 4; i += 3)
  {
    var triplet = (((binarray[i   >> 2] >> 8 * ( i   %4)) & 0xFF) << 16)
                | (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 )
                |  ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF);
    for(var j = 0; j < 4; j++)
    {
      if(i * 8 + j * 6 > binarray.length * 32) str += b64pad;
      else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);
    }
  }
  return str;
}