/**
 * @fileOverview
 * A collection of array utility functions.
 * @name SitePoint.Arrays
 * @author Craig Anderson <craig@sitepoint.com>
 */

/**
 * The SitePoint namespace.
 * @namespace
 * @ignore
 */
var SitePoint = SitePoint || {};

/**
 * Namespace for simple array utility functions.
 * @namespace
 */
SitePoint.Arrays = SitePoint.Arrays || {};

/**
 * Determine whether the given variable is an array.
 * @param {object} variable The variable.
 * @return {boolean} True is variable is an array, false otherwise.
 * @author Craig Anderson <craig@sitepoint.com>
 * @static
 */
SitePoint.Arrays.IsArray = function(variable) {
	return SitePoint.Type.IsArray(variable);
};

/**
 * Sort an array. The array will be sorted in-memory (i.e. the array passed in will be sorted, this function will not return a new array).
 * @param {array} array The array to sort.
 * @param {function} sortFunction The function to call to sort the entries in the array. If omitted, a simple < function will be used. This function will recieve two parameters (a and b), and should return > 0 if a > b, < 0 if a < b.
 * @author Craig Anderson <craig@sitepoint.com>
 * @static
 */
SitePoint.Arrays.Sort = function(array, sortFunction) {
	try {
		// Check the array is an array
		if(!SitePoint.Type.IsArray(array)) {
			throw new TypeError();
		}
		
		// Set the sort function if it's not defined.
		if(typeof sortFunction != "function") {
			sortFunction = function(a, b) {
				return b - a;
			};
		}
		
		// Perform sort
		var noItems = array.length;
		for(var i = 0; i < noItems; i++) {
			var itemA = array[i];
			for(var j = 0; j < i; j++) {
				if(j != i) {
					var itemB = array[j];
					if(sortFunction(itemA, itemB) > 0) {
						// swap items
						var temp = array[i];
						array[i] = array[j];
						array[j] = temp;
						itemA = array[i];
						itemB = array[j];
					}
				}
			}
		}
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Arrays.Sort");
		console.log(e);
		throw e;
	}
};

/**
 * Remove duplicates from an array.
 * @param {array} array The array to remove duplicates from.
 * @param {function} [equalityFunction] An optional function which checks for
 * equality of two items. It should return true when two items are equal, and
 * false otherwise. By default, this will be a '===' equality check.
 * @author Craig Anderson <craig@sitepoint.com>
 * @static
 */
SitePoint.Arrays.RemoveDuplicates = function(array, equalityFunction) {
	try {
		if(typeof equalityFunction != "function") {
			equalityFunction = function(a, b) {
				return a === b;
			};
		}
		var noItems = array.length;
		for(var i = 0; i < noItems; i++) {
			var a = array[i];
			for(var j = (i + 1); j < noItems; j++) {
				var b = array[j];
				if(equalityFunction(a, b)) {
					// b is a duplicate of a
					// remove this element from the array
					array.splice(j, 1);
					noItems--;
					j--;
				}
			}
		}
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Arrays.RemoveDuplicates");
		console.log(e);
		throw e;
	}
};
/**
 * @fileOverview
 * Adds a clone method to all objects. Requires SitePoint.Type.
 * Note that this does not work particularly well with DOM objects. Recommend
 * using jQuery's clone method instead.
 * @name SitePoint.Clone
 * @author Craig Anderson <craig@sitepoint.com>
 */

/**
 * The SitePoint namespace.
 * @namespace
 * @ignore
 */
var SitePoint = SitePoint || {};

/**
 * Clone any object.
 * Script adapted from http://www.faqts.com/knowledge_base/view.phtml/aid/6231
 * @param {integer} [maxDepth] How deep to clone. 1 by default. If supplied as the boolean true, this will be set to 100.
 * @return {object} The clone of this object.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.clone = function(element, depth) {
	var theClone = element;
	
	if(typeof depth == "boolean") {
		depth = 100;
	}
	depth = parseInt(depth);
	if(isNaN(depth)) {
		depth = 1;
	}
	
	if(depth > 0) {
		depth--;
		if(SitePoint.Type.IsArray(element)) {
			theClone = [];
			var noItems = element.length;
			for(var index = 0; index < noItems; index++) {
				var item = element[index];
				if(typeof item == "object" && item !== null) {
					theClone[index] = SitePoint.clone(item, depth);
				}
				else {
					theClone[index] = item;
				}
			}
		}
		else
		{
			theClone = {};
			for(var property in element) {
				if(typeof element[property] == "object" && element[property] !== null ) {
					try {
						if(property != "ownerDocument" && property != "ownerElement") {
							theClone[property] = SitePoint.clone(element[property], depth);
						}
					}
					catch(e) {
						console.log("An error occurred cloning the property " + property + " of the following object.");
						console.log(element);
						throw e;
					}
				}
				else {
					theClone[property] = element[property];
				}
			}
		}
	}
	
	return theClone;
};
/**
 * @fileOverview
 * When used in conjunction with the SitePoint.Events.EventManager, this will
 * cause event handlers to be handled only once for each configurable delay
 * period.
 * @name SitePoint.Events.DelayedManagementScheme
 */

/**
 * The SitePoint namespace.
 * @namespace
 * @ignore
 */
var SitePoint = SitePoint || {};

/**
 * The SitePoint Events namespace.
 * @namespace
 * @ignore 
 */
SitePoint.Events = SitePoint.Events || {};

/**
 * @class An event management scheme which delays event execution.
 * Under this scheme, events will only fire at most every one second (this
 * delay time is configurable). If two events are fired before this one second
 * delay has passed, the second event will be stored, and then fired in one
 * second's time. Subsequent events are lost.
 * Note that this constructor should not be called directly; call
 * SitePoint.Events.DelayedManagementScheme.GetInstance instead.
 * @constructor
 * @param {int} [delayTime] The minimum amount of time, in milliseconds, between
 * event handlers being invoked. If omitted, this is set to 1000 (one second).
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.DelayedManagementScheme = function(delayTime) {
	try {
		if(SitePoint.Events.DelayedManagementScheme._NoInstances > 0) {
			throw new Error("A DelayedManagementScheme has already been instantiated.\n\n" +
					"Please make use of that instance of the class, or access it via " +
					"the SitePoint.Events.DelayedManagementScheme.GetInstance method.");
		}
		
		// Initialise the hashtables which will store our timeout handles and
		// the event handlers.
		this._eventData = new SitePoint.HashTable();
		this._handlers = new SitePoint.HashTable();
		this._timeoutHandles = new SitePoint.HashTable();
		
		// Initialise delay time
		if(typeof delayTime != "number") {
			delayTime = 1000; // 1 second
		}
		this._delayTime = delayTime;
		
		// Signify that this instance has been created and store this instance
		SitePoint.Events.DelayedManagementScheme._NoInstances++;
		SitePoint.Events.DelayedManagementScheme._Instance = this;
	}
	catch(e) {
		console.log("An unhandled error occurred in the SitePoint.Events.DelayedManagementScheme constructor.");
		console.log(e);
		throw e;
	}
};

/**
 * @property {int} _delayTime The configurable delay time.
 * @private
 */
SitePoint.Events.DelayedManagementScheme.prototype._delayTime = null;

/**
 * @property {SitePoint.HashTable} _eventData Event data for delayed events.
 * @private
 */
SitePoint.Events.DelayedManagementScheme.prototype._eventData = null;

/**
 * @property {SitePoint.HashTable} _handlers The SitePoint.Events.EventManager.ManagedHandlers for delayed events.
 * @private
 */
SitePoint.Events.DelayedManagementScheme.prototype._handlers = null;

/**
 * @property {int} _timeoutHandles Timeout handles for delayed events
 * @private
 */
SitePoint.Events.DelayedManagementScheme.prototype._timeoutHandles = null;

/**
 * @property {int} _NoInstances The current number of instances of this class.
 * Should never exceed 1.
 * @static
 * @private
 */
SitePoint.Events.DelayedManagementScheme._NoInstances = 0;

/**
 * @property {SitePoint.Events.DelayedManagementScheme} _Instance The current instance of this class if it exists.
 * @static
 * @private
 */
SitePoint.Events.DelayedManagementScheme._Instance = null;

/**
 * Called by the SitePoint.Events.EventManager whenever an event is raised.
 * This method will either invoke the given ManagedHandler, set a timeout
 * to invoke the ManagedHandler after the configured delay, or completely
 * ignore the event if a ManagedHandler has already been scheduled for
 * execution.
 * @param {SitePoint.Events.EventManager.ManagedHandler} handler
 * @param {object} eventData The event data supplied by the browser.
 * @return {boolean} True if the event was managed, false otherwise.
 * Note that this function will always return true.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.DelayedManagementScheme.prototype.manage = function(handler, eventData) {
	try {
		var key = this._generateKey(handler);
		var timeoutHandle = this._timeoutHandles.get(key);
		if(handler.getTimeSinceLastInvocation() > this._delayTime ||
				this._delayTime <= 0) {
			// Handler hasn't been invoked in the last second.
			// Clear any existing timeout handle
			clearTimeout(timeoutHandle);
			this._eventData.remove(key);
			this._handlers.remove(key);
			this._timeoutHandles.remove(key);
			// Invoke the handler
			handler.invoke(eventData);
		}
		else {
			// Handler has been invoked in the last second
			if(!timeoutHandle) {
				// Event has been invoked in the last second, but no timeout has been
				// set.
				// Set the timeout to call HandleTimeout.
				var functionString = "SitePoint.Events.DelayedManagementScheme.HandleTimeout(\"" + key + "\");";
				timeoutHandle = setTimeout(functionString, this._delayTime - 10);
				this._eventData.add(key, eventData);
				this._handlers.add(key, handler);
				this._timeoutHandles.add(key, timeoutHandle);
			}
		}
		return true; // notify manager that the event has been managed by this scheme
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Events.DelayedManagementScheme::manage");
		console.log(e);
		throw e;
	}
};

/**
 * Invoke a delayed event.
 * This method will be called by the static
 * SitePoint.Events.DelayedManagedScheme.HandleTimeout method.
 * @param {string} key The key the event data, handler, and timeout handle is
 * stored with.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.DelayedManagementScheme.prototype.handleTimeout = function(key) {
	try {
		var eventData = this._eventData.get(key);
		var handler = this._handlers.get(key);
		try {
			handler.invoke(eventData);
		}
		catch(e) {
			console.log("An unhandled error occurred while invoking an event handler");
			console.log(e);
			this._eventData.remove(key);
			this._handlers.remove(key);
			this._timeoutHandles.remove(key);
			throw e;
		}
		
		this._eventData.remove(key);
		this._handlers.remove(key);
		this._timeoutHandles.remove(key);
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Events.DelayedManagementScheme::handleTimeout");
		console.log(e);
		throw e;
	}
};

/**
 * Unload the event manager, returning it to it's initial state.
 * @return Nothing.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.DelayedManagementScheme.prototype.unload = function() {
	try {
		this._eventData.unload();
		this._handlers.unload();
		this._timeoutHandles.unload();
		
		SitePoint.Events.DelayedManagementScheme._NoInstances--;
		SitePoint.Events.DelayedManagementScheme._Instance = null;
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.Events.DelayedManagementScheme::receiveEvent");
		console.log(e);
		throw e;
	}
};

/**
 * Generate a key for a given managed handler.
 * @param {SitePoint.Events.EventManager.ManagedHandler} handler
 * @return {string} A unique ID for this object-eventName pair
 * @author Craig Anderson <craig@sitepoint.com>
 * @private
 */
SitePoint.Events.DelayedManagementScheme.prototype._generateKey = function(handler) {
	try {
		var object = handler.getObject();
		
		var key = "eventName=" + handler.getEventName() + ";";
		if(SitePoint.Type.IsHtmlElement(object)) {
			// object is an HTML element
			key += "tagName=" + object.tagName + ";";
			key += "id=" + object.getAttribute("id") + ";";
			key += "name=" + object.getAttribute("name") + ";";
		}
		else {
			// object is not an element
			key += "string=" + object.toString() + ";";
		}
		
		return key;
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.Events.DelayedManagementScheme::_generateKey");
		console.log(e);
		throw e;
	}
};


SitePoint.Events.DelayedManagementScheme.GetInstance = function() {
	if(SitePoint.Events.DelayedManagementScheme._NoInstances > 0) {
		return SitePoint.Events.DelayedManagementScheme._Instance;
	}
	else {
		return new SitePoint.Events.DelayedManagementScheme();
	}
};

SitePoint.Events.DelayedManagementScheme.HandleTimeout = function(key) {
	var scheme = SitePoint.Events.DelayedManagementScheme.GetInstance();
	scheme.handleTimeout(key);
};
/**
 * @fileOverview
 * An event manager which allows for pluggable management schemes.
 * @name SitePoint.Events.EventManager
 * @author Craig Anderson <craig@sitepoint.com>
 */


/**
 * The SitePoint namespace.
 * @namespace
 * @ignore
 */
var SitePoint = SitePoint || {};

/**
 * The SitePoint Events namespace.
 * @namespace
 */
SitePoint.Events = SitePoint.Events || {};

// Create Firebug stub if required
// Note that we should only use Firebug functions listed below.
window.console = window.console || { log: function() { /* do nothing */ } };

/**
 * @class An event manager.
 * Register event handlers using the register method.
 * You can set up event management schemes by calling registerManagementScheme.
 * There are management scheme objects, such as the DelayedManagementScheme,
 * which can be used with this object.
 * Note that this constructor should never be called directly. To get an
 * instance of this class, call SitePoint.Events.EventManager.GetInstance().
 * Note that this class requires SitePoint.Type and SitePoint.HashTable.
 * @example
 * // Register an event to be managed
 * eventManager.register(theButton, "click", function() { alert("Click!"); });
 * @example
 * // Register some management schemes
 * eventManager.registerManagementScheme(function(handler, eventData) {
 * 	// Only manage click events
 * 	if(handler.getEventType() == "click") {
 * 		// Manage this event by calling the handler
 * 		handler.invoke(eventData);
 * 		return true;
 * 	}
 * 	else {
 * 		// Do not manage this event
 * 		return false;
 * 	}
 * });
 * // Register the delayed event management scheme
 * eventManager.registerManagementScheme(SitePoint.Events.DelayedManagementScheme.GetInstance());
 * @constructor
 * @return Nothing.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.EventManager = function() {
	try {
		if(SitePoint.Events.EventManager._NoInstances > 0) {
			throw new Error("An EventManager has already been instantiated.\n\n" +
					"Please make use of that instance of the class, or access it via " +
					"the SitePoint.Events.EventManager.GetInstance method.");
		}
		
		// Initialise the hash table which will store all of our managed handlers.
		this._managedHandlers = new SitePoint.HashTable();
		
		// Initialise the array of management schemes.
		this._managementSchemes = [];
		
		// Signify that this instance has been created and store this instance
		SitePoint.Events.EventManager._NoInstances++;
		SitePoint.Events.EventManager._Instance = this;
	}
	catch(e) {
		console.log("An unhandled error occurred in the SitePoint.Events.EventManager constructor.");
		console.log(e);
		throw e;
	}
};

/**
 * @property {SitePoint.HashTable} _managedHandlers TODO: Document this property.
 * @private
 */
SitePoint.Events.EventManager.prototype._managedHandlers = null;

/**
 * @property {array} _managementSchemes TODO: Document this properly.
 * @private
 */
SitePoint.Events.EventManager.prototype._managementSchemes = null;

/**
 * @property {int} _NoInstances The current number of instances of this class.
 * Should never exceed 1.
 * @static
 * @private
 */
SitePoint.Events.EventManager._NoInstances = 0;

/**
 * @property {SitePoint.Events.EventManager} _Instance The current instance of this class if it exists.
 * @static
 * @private
 */
SitePoint.Events.EventManager._Instance;

/**
 * Register a managed event handler.
 * @example
 * // Register an event to be managed
 * eventManager.register(theButton, "click", function() { alert("Click!"); });
 * @param {object} object The object to attach the event handler to.
 * @param {string} eventName The name of the event to listen for.
 * This event name should not include the IE-style "on" prefix.
 * @param {function} handler The function to call when this event is raised
 * and allowed by any registered management schemes.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.EventManager.prototype.register = function(object, eventName, handler) {
	try {
		// Ensure all parameters are of the correct type
		if(typeof object != "object" || typeof eventName != "string" ||
				typeof handler != "function") {
			throw new TypeError();
		}
		
		var key = this._generateKey(object, eventName);
		var managedHandler = new SitePoint.Events.EventManager.ManagedHandler(object, eventName, handler);
		
		// Determine if an event handler has already been added for this event
		var existingHandler = this._managedHandlers.get(key);
		if(existingHandler) {
			// Get the last handler in this chain of handlers, then add this
			// one to the end.
			var lastHandler = existingHandler.getLastHandler();
			lastHandler.setNextHandler(managedHandler);
		}
		else {
			// No handler currently exists. Add this handler to the hashtable.
			this._managedHandlers.add(key, managedHandler);
		}
		
		// Listen to events for this object.
		this._addEventListener(object, eventName);
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.Events.EventManager::register");
		console.log(e);
		throw e;
	}
};

/**
 * Register an event management scheme.
 * An event management scheme is responsible for invoking event handlers. When
 * an event management scheme manages any event, it should return true. If
 * false is returned, the next management scheme is executed. If no management
 * schemes return true, the event handler is invoked.
 * @param {function | object} managementScheme A function which accepts a
 * SitePoint.Events.EventManager.ManagedHandler as its only parameter,
 * returns true when the given event has been managed, and false otherwise.
 * Can also be an object with a method manage which does the same.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.EventManager.prototype.registerManagementScheme = function(managementScheme) {
	try {
		// managementScheme can be a function, or an object with a method named
		// manage.
		var managementSchemeOk = false;
		if(typeof managementScheme == "function") {
			managementSchemeOk = true;
		}
		if(typeof managementScheme == "object" &&
				typeof managementScheme.manage == "function") {
			managementSchemeOk = true;
		}
		if(!managementSchemeOk) {
			throw new TypeError();
		}
		
		// Add this management scheme to the given list of management schemes.
		this._managementSchemes.push(managementScheme);
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.Events.EventManager::register");
		console.log(e);
		throw e;
	}
};

/**
 * Receive an event. Note that this should not be called directly.
 * @param {object} object
 * @param {string} eventName
 * @param {object} eventData
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.EventManager.prototype.receiveEvent = function(object, eventName, eventData) {
	try {
		// Look up the handler for this event
		var key = this._generateKey(object, eventName);
		var handler = this._managedHandlers.get(key);
		if(!handler) {
			throw new Error("Could not find a handler for this event");
		}
		// Iterate through the list of handlers.
		do {
			this._manageEvent(handler, eventData);
			handler = handler.getNextHandler();
		} while(handler);
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.Events.EventManager::receiveEvent");
		console.log(e);
		throw e;
	}
};

/**
 * Unload the event manager, returning it to it's initial state.
 * @return Nothing.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.EventManager.prototype.unload = function() {
	try {
		// Unload the management schemes
		while(this._managementSchemes.length > 0) {
			this._managementSchemes.pop();
		}
		
		// Detach the event listeners from the elements
		var theEventManager = this;
		this._managedHandlers.each(function(managedHandler) {
			var object = managedHandler.getObject();
			var eventName = managedHandler.getEventName();
			theEventManager._removeEventListener(object, eventName);
		});
		theEventManager = null;
		
		// Unload the managed handlers
		this._managedHandlers.unload();
		
		SitePoint.Events.EventManager._NoInstances--;
		SitePoint.Events.EventManager._Instance = null;
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.Events.EventManager::receiveEvent");
		console.log(e);
		throw e;
	}
};

/**
 * Generate a key for a given object-eventName pair.
 * @param object The object.
 * @param eventName The event.
 * @return A string uniquely describing this object-event pair.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.EventManager.prototype._generateKey = function(object, eventName) {
	try {
		// Ensure all parameters are of the correct type
		if(typeof object != "object" || typeof eventName != "string") {
			throw new TypeError();
		}

		var key = "eventName=" + eventName + ";";
		if(SitePoint.Type.IsHtmlElement(object)) {
			// object is an HTML element
			key += "tagName=" + object.tagName + ";";
			key += "id=" + object.getAttribute("id") + ";";
			key += "name=" + object.getAttribute("name") + ";";
		}
		else {
			// object is not an element
			key += "string=" + object.toString() + ";";
		}
		
		return key;
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.Events.EventManager::_generateKey");
		console.log(e);
		throw e;
	}
};


/**
 * Add an event listener to the given object for the given event.
 * @param object The object to add the listener to.
 * @param event The event to listen for.
 * @return Nothing
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.EventManager.prototype._addEventListener = function(object, eventName) {
	try {
		// Ensure all parameters are of the correct type
		if(typeof object != "object" || typeof eventName != "string") {
			throw new TypeError();
		}

		if(typeof object.addEventListener == "function") {
			object.addEventListener(eventName, SitePoint.Events.EventManager.ReceiveW3CEvent, false);
		}
		else {
			object.attachEvent("on" + eventName, SitePoint.Events.EventManager.ReceiveMicrosoftEvent);
		}
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.Events.EventManager::_addEventListener");
		console.log(e);
		throw e;
	}
};

/**
 * Remove an existing event listener from given object for the given event.
 * @param object The object to remove the listener from.
 * @param event The event to listen for.
 * @return Nothing
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.EventManager.prototype._removeEventListener = function(object, eventName) {
	try {
		// Ensure all parameters are of the correct type
		if(typeof object != "object" || typeof eventName != "string") {
			throw new TypeError();
		}

		if(typeof object.removeEventListener == "function") {
			object.removeEventListener(eventName, SitePoint.Events.EventManager.ReceiveW3CEvent, false);
		}
		else {
			object.detachEvent("on" + eventName, SitePoint.Events.EventManager.ReceiveMicrosoftEvent);
		}
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.Events.EventManager::_removeEventListener");
		console.log(e);
		throw e;
	}
};


/**
 * Manage the given event.
 * @param handler
 * @param eventData
 * @return Nothing
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.EventManager.prototype._manageEvent = function(handler, eventData) {
	try {
		// Iterate through each of the given management schemes, stopping once
		// the event has been managed.
		var noManagementSchemes = this._managementSchemes.length;
		var eventManaged = false;
		for(var i = 0; i < noManagementSchemes; i++) {
			// Call the management scheme
			var managementScheme = this._managementSchemes[i];
			try {
				if(typeof managementScheme == "function") {
					eventManaged = managementScheme(handler, eventData);
				}
				else {
					eventManaged = managementScheme.manage(handler, eventData);
				}
			}
			catch(e) {
				console.log("An unexpected error occurred in an event management scheme");
				console.log(e);
				throw e;
			}
			
			// If the event was managed, break out of the for loop.
			if(eventManaged) {
				break;
			}
		}
		
		// If the event hasn't been managed, just invoke it.
		if(!eventManaged) {
			handler.invoke(eventData);
		}
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.Events.EventManager::_manageEvent");
		console.log(e);
		throw e;
	}
};

/**
 * Get the instance of this class or create a new one.
 * @param {boolean} [allowCreation] Whether to create an instance if one doesn't already exist. Default to true. If set to false and an instance isn't already created, null is returned.
 * @return {SitePoint.Events.EventManager}
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.EventManager.GetInstance = function(allowCreation) {
	if(typeof allowCreation == "undefined") {
		allowCreation = true;
	}
	if(SitePoint.Events.EventManager._NoInstances > 0) {
		return SitePoint.Events.EventManager._Instance;
	}
	else {
		if(allowCreation) {
			return new SitePoint.Events.EventManager();
		}
		else {
			return null;
		}
	}
};

SitePoint.Events.EventManager.ReceiveW3CEvent = function(eventData) {
	try {
		var eventTarget = eventData.target;
		var eventName = eventData.type;
		var man = SitePoint.Events.EventManager.GetInstance(false); // do not allow creation
		if(man) {
			man.receiveEvent(eventTarget, eventName, eventData);
		}
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.Events.EventManager.ReceiveW3CEvent");
		console.log(e);
		throw e;
	}
};

SitePoint.Events.EventManager.ReceiveMicrosoftEvent = function(eventData) {
	try {
		if(typeof eventData == "undefined") {
			eventData = window.event;
		}
		var eventTarget = eventData.srcElement;
		if(!eventTarget) {
			eventTarget = window;
		}
		var eventName = eventData.type;
		var man = SitePoint.Events.EventManager.GetInstance(false); // do not allow creation
		if(man) {
			man.receiveEvent(eventTarget, eventName, eventData);
		}
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.Events.EventManager.ReceiveMicrosoftEvent");
		console.log(e);
		throw e;
	}
};

/**
 * A wrapper for an event handler. This class contains metadata describing
 * various aspects of the event handler, such as when it was last invoked,
 * what object it is attached to, and so on.
 * @param {object} object The object handled by this managed handler.
 * @param {string} eventName The event handled by this managed handler.
 * @param {function} handler The function to be called when this event is invoked.
 * @author Craig Anderson <craig@sitepoint.com>
 * @constructor
 */
SitePoint.Events.EventManager.ManagedHandler = function(object, eventName, handler) {
	try {
		if(typeof object != "object" || typeof eventName != "string" ||
				typeof handler != "function") {
			throw new TypeError();
		}
		
		this._handler = handler;
		this._object = object;
		this._eventName = eventName;
		this._lastInvoked = new Date(0); // Initialise to 1 Jan 1970.
	}
	catch(e) {
		console.log("An unexpected error occurred in the SitePoint.Events.EventManager.Event constructor");
		console.log(e);
		throw e;
	}
};

SitePoint.Events.EventManager.ManagedHandler.prototype._object = null;
SitePoint.Events.EventManager.ManagedHandler.prototype._eventName = null;
SitePoint.Events.EventManager.ManagedHandler.prototype._handler = null;
SitePoint.Events.EventManager.ManagedHandler.prototype._lastInvoked = null;
SitePoint.Events.EventManager.ManagedHandler.prototype._nextHandler = null;

/**
 * Get the object this event handler is attached to.
 * @return The object this event handler is attached to.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.EventManager.ManagedHandler.prototype.getObject = function() {
	try {
		return this._object;
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.Events.EventManager.ManagedHandler::getObject");
		console.log(e);
		throw e;
	}
};

/**
 * Get the object this event handler is attached to.
 * @deprecated Stopped using the 'element' nomenclature as events are not
 * always associated with elements (for example, window or document events).
 * You should use getObject instead.
 * @return {object} The object this event handler is attached to.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.EventManager.ManagedHandler.prototype.getElement = function() {
	return this.getObject();
};

/**
 * Get the name of the event this handler is attached to.
 * @return The name of the event this handler is attached to.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.EventManager.ManagedHandler.prototype.getEventName = function() {
	try {
		return this._eventName;
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.Events.EventManager.ManagedHandler::getEventName");
		console.log(e);
		throw e;
	}
};

/**
 * Get the date this handler was last invoked.
 * @return A JavaScript Date object.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.EventManager.ManagedHandler.prototype.getDateLastInvoked = function() {
	try {
		return this._lastInvoked;
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.Events.EventManager.ManagedHandler::getDateLastInvoked");
		console.log(e);
		throw e;
	}
};

/**
 * Get the number of milliseconds since this handler was last invoked.
 * @return The number of milliseconds since this handler was last invoked.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.EventManager.ManagedHandler.prototype.getTimeSinceLastInvocation = function() {
	try {
		return (new Date()).getTime() - this._lastInvoked.getTime();
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.Events.EventManager.ManagedHandler::getDateLastInvoked");
		console.log(e);
		throw e;
	}
};

/**
 * Invoke this handler.
 * @param eventData The event data passed to the event handler by the browser.
 * @return Nothing
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.EventManager.ManagedHandler.prototype.invoke = function(eventData) {
	try {
		try {
			this._handler(this._object, eventData);
		}
		catch(e) {
			console.log("An unexpected error occurred in a managed handler function");
			console.log(e);
			throw e;
		}
		this._lastInvoked = new Date();
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.Events.EventManager.ManagedHandler::invoke");
		console.log(e);
		throw e;
		
	}
};

/**
 * Get the next handler, if one exists.
 * @return Either a ManagedHandler or null.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.EventManager.ManagedHandler.prototype.getNextHandler = function() {
	try {
		return this._nextHandler;
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.Events.EventManager.ManagedHandler::invoke");
		console.log(e);
		throw e;
	}
};

/**
 * Set the next handler. Note that this will throw an exception if a next 
 * handler has already been set.
 * @param handler The handler to set as the next handler.
 * @return Nothing
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.EventManager.ManagedHandler.prototype.setNextHandler = function(handler) {
	try {
		// If there is already a next handler, throw an exception.
		if(this._nextHandler) {
			throw new Error("Cannot set a next handler when one already exists");
		}
		// Check that handler is actually a managed handler.
		if(typeof handler != "object" || typeof handler.getObject != "function") {
			throw new TypeError("setNextHandler can only accept a ManagedHandler");
		}
		// Set the next handler.
		this._nextHandler = handler;
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.Events.EventManager.ManagedHandler::setNextHandler");
		console.log(e);
		throw e;
	}
};

/**
 * Get the last handler in the chain of handlers.
 * @return A ManagedHandler.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Events.EventManager.ManagedHandler.prototype.getLastHandler = function() {
	try {
		if(this.getNextHandler()) {
			return this.getNextHandler().getLastHandler();
		}
		else {
			return this;
		}
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.Events.EventManager.ManagedHandler::getLastHandler");
		console.log(e);
		throw e;
	}
};
/**
 * @fileOverview
 * A JavaScript hash table tuned for performance of it's get method. It's much
 * more performant than a simple array-based implementation
 * (lookups in SitePoint.HashTable are O(1) in the best case, O(n) in the worst
 * case; lookups for a simple array-based implementation are always O(n)), and
 * it can store values against *any* key.
 * @name SitePoint.HashTable
 * @author Craig Anderson craig@sitepoint.com
 */

/**
 * The SitePoint namespace.
 * @namespace
 * @ignore
 */
var SitePoint = SitePoint || {};

// Create Firebug stub if required
// Note that we should only use Firebug functions listed below.
window.console = window.console || { log: function() { /* do nothing */ } };

/**
 * @class A JavaScript hash table implementation optimised for performance
 * of its get method.
 * @constructor
 * @param {int} [noBuckets] The number of buckets to initially create.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.HashTable = function(noBuckets) {
	try {
		// Create the buckets for this hash table.
		if(!noBuckets) {
			noBuckets = SitePoint.HashTable._DefaultNoBuckets;
		}
		this._buckets = new Array(noBuckets);
		
		// Initialise the hash keys generated flag to false.
		this._hashKeysGenerated = false;
		
		// Create the default hash function.
		this.setHashFunction(function(s) {
			var hash = 0;
			if(s !== null) {
				hash = s.toString().length;
			}
			return hash;
		});
		
		// Initialise the list of keys. This list is required for toArray.
		this._keys = [];
	}
	catch(e) {
		console.log("An unhandled error occurred in the SitePoint.HashTable constructor.");
		console.log(e);
		throw e;
	}
};

/**
 * @property {int} _DefaultNoBuckets The default number of buckets in a HashTable.
 * @private
 * @static
 */
SitePoint.HashTable._DefaultNoBuckets = 50;

/**
 * @property {array} _buckets The buckets which make up the hash table.
 * @private
 */
SitePoint.HashTable.prototype._buckets = null;

/**
 * @property {function} _hashFunction The hash function.
 * @private
 */
SitePoint.HashTable.prototype._hashFunction = null;

/**
 * @property {boolean} _hashKeysGenerated Whether the hash function has been
 * called. If the hash function has already been called, it cannot be changed
 * as this would invalidate any existing hashes.
 * @private
 */
SitePoint.HashTable.prototype._hashKeysGenerated = null;

/**
 * @property {array} _keys An array of keys used in this hash table. This is
 * used primarily to allow conversion of this hash table into an array.
 * @private
 */
SitePoint.HashTable.prototype._keys = null;

/**
 * Get a value from the hash table.
 * @param {object} key
 * @return {object} The value, or null if the key could not be found.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.HashTable.prototype.get = function(key) {
	try {
		var value = null;
		// Get the bucket the requested value would be in
		var bucket = this._getBucket(key);
		// Search the given bucket for the entry in question
		var noEntries = bucket.length;
		for(var i = 0; i < noEntries; i++) {
			var entry = bucket[i];
			if(entry.getKey() == key) {
				value = entry.getValue();
				break; // get out of for loop
			}
		}
		return value;
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.HashTable::get");
		console.log(e);
		throw e;
	}
};

/**
 * Add an entry to the hash table. Any existing entry with the
 * given key will be overwritten. If you don't want this to happen, you
 * should check that get returns null for the given key, as shown below.
 * @example function noReplaceAdd(hashTable, key, value) {
 *   var oldValue = hashTable.get(key);
 *   if(oldValue !== null) {
 *     // There is already a value for this key in the hash table.
 *     throw new Error();
 *   }
 *   else {
 *     hashTable.add(key, value);
 *   }
 * }
 * @param {object} key
 * @param {object} value
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.HashTable.prototype.add = function(key, value) {
	try {
		// First remove any existing value for this key
		this.remove(key);
		// Now add the value
		var bucket = this._getBucket(key);
		bucket.push(new SitePoint.HashTable.Entry(key, value));
		// Record this key
		this._keys.push(key);
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.HashTable::add");
		console.log(e);
		throw e;
	}
};

/**
 * Remove an entry from the hash table.
 * @param {object} key The key of the entry to remove.
 * @param {boolean} [callUnload] Whether to call the unload method of the entry. If omitted, this default to true.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.HashTable.prototype.remove = function(key, callUnload) {
	try {
		if(typeof callUnload == "undefined") {
			callUnload = true;
		}
		var bucket = this._getBucket(key);
		var noObjects = bucket.length;
		for(var i = 0; i < noObjects; i++) {
			var entry = bucket[i];
			if(entry.getKey() == key) {
				// Found the entry
				// Unload the entry
				if(callUnload) {
					entry.unload();
				}
				// Remove the entry from the bucket
				bucket.splice(i, 1);
				// Exit this for loop
				break;
			}
		}
		// Remove this key from the _keys array
		var noKeys = this._keys.length;
		for(var j = 0; j < noKeys; j++) {
			if(this._keys[j] == key) {
				// Found the key
				this._keys.splice(j, 1); // remove the key from the array
			}
		}
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.HashTable::remove");
		console.log(e);
		throw e;
	}
};

/**
 * Set the function used to generate hash keys.
 * This can be any function which always returns any integer
 * consistently for any given input. Note that this function cannot
 * be called after callHashFunction has been called, as changing
 * the hash function after hashes have been generated would
 * invalidate the previously generated hashes.
 * @example // --------------------------------------
 * // VALID HASH FUNCTIONS
 * // --------------------------------------
 * 
 * function linear() {
 *   return 5;
 * }
 * 
 * function stringLength(x) {
 *   // This is actually the default hash function
 *   return x.toString().length;
 * }
 * 
 * function integerKeysOnly(x) {
 *   if(!/^\d+$/.test(x)) {
 *     throw new TypeError("Key must be an integer")'
 *   }
 *   return x;
 * }
 * 
 * function veryLargeKeys(x) {
 *   return x.toString().length * 10000;
 * }
 * 
 * function negativeKeys(x) {
 *   return x.toString().length * -1;
 * }
 * 
 * // --------------------------------------
 * // INVALID HASH FUNCTIONS
 * // --------------------------------------
 * 
 * function randomInt() {
 *   return Math.floor(Math.random() * 11); // random number >= 0 <= 10
 * }
 * 
 * function timeStamp() {
 *   return (new Date()).getTime();
 * }
 * 
 * function combo(x) {
 *   return x.toString().length * (new Date()).getTime();
 * }
 * @param {function} f The hash function.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.HashTable.prototype.setHashFunction = function(f) {
	try {
		// Check a hash key has not already been generated
		if(this._hashKeysGenerated) {
			throw new Error("A hash key has already been generated for this table, so the hash function cannot be changed.");
		}
		// Check that a function was supplied.
		if(!f || typeof(f) != "function") {
			throw new TypeError("Invalid or missing parameter");
		}
		// Check that function will return an integer
		// TODO: Add more robust testing of the hash function
		var callSucceeded = false;
		var testHash = NaN;
		try {
			testHash = f("test");
			callSucceeded = true;
		}
		catch(e) {
			callSucceeded = false;
		}
		if(!callSucceeded || !(/^\d+$/.test(testHash))) {
			throw new Error("The given function does not behave as required.");
		}
		// Set the hash function.
		this._hashFunction = f;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.HashTable::setHashFunction");
		console.log(e);
		throw e;
	}
};

/**
 * Call the registered hash function. This wrapper will ensure that the
 * returned value is a valid bucket index, and will stop subsequent changes
 * to the hash function.
 * @param {object} key The key to pass to the hash function.
 * @return {int} The hash.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.HashTable.prototype.callHashFunction = function(key) {
	try {
		var hash = 0;
		try {
			hash = this._hashFunction(key);
		}
		catch(e) {
			console.log("An unhandled error occurred in the registered hash function");
			console.log(e);
			throw e;
		}
		hash = hash % this._buckets.length;
		if(hash < 0) {
			hash = hash * -1;
		}
		this._hashKeysGenerated = true;
		return hash;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.HashTable::callHashFunction");
		console.log(e);
		throw e;
	}
};

/**
 * Generate an array representation of this hash table. The first
 * element will be the first value added, and so on. Note that this will *not*
 * generate an associative array (see example below).
 * @example // Add some stuff to a hash table
 * hashTable.add("foo", "bar");
 * hashTable.add("aStringKey", 55);
 * hashTable.add(0, "aStringValue");
 * // Create an array
 * var arr = hashTable.toArray();
 * print(arr[0]);            // prints "bar"
 * print(arr[1]);            // prints 55
 * print(arr[2]);            // prints "aStringValue"
 * print(arr["foo"]);        // error!
 * print(arr["aStringKey"]); // error!
 * print(arr[55]);           // error!
 * @return {array} An array representation of this hash table.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.HashTable.prototype.toArray = function() {
	try {
		var array = [];
		var noKeys = this._keys.length;
		for(var i = 0; i < noKeys; i++) {
			var key = this._keys[i];
			var value = this.get(key);
			array.push(value);
		}
		return array;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.HashTable::toArray");
		console.log(e);
		throw e;
	}
};

/**
 * Get a list of keys defined for this hash table.
 * @return {array} An array of keys defined for this hash table.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.HashTable.prototype.getKeys = function() {
	try {
		return this._keys;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.HashTable::getKeys");
		console.log(e);
		throw e;
	}
};


/**
 * Call the given function for each entry in the hash table.
 * @param {function} f The function to call. It will be passed two paramters:
 * the item, and the item's key.
 * @example
 * hashTable.each(function(item, key) {
 * 	alert(key + " = " + item);
 * });
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.HashTable.prototype.each = function(f) {
	try {
		if(typeof f != "function") {
			throw new TypeError();
		}
		var noKeys = this._keys.length;
		for(var i = 0; i < noKeys; i++) {
			var key = this._keys[i];
			var item = this.get(key);
			try {
				f(item, key);
			}
			catch(e) {
				console.log("An unhandled error occurred in the function passed into SitePoint.HashTable::each");
				console.log(e);
				throw e;
			}
		}
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.HashTable::each");
		console.log(e);
		throw e;
	}
};

/**
 * Get the number of elements in this hash table.
 * @return {integer} The number of elements in this hash table.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.HashTable.prototype.count = function() {
	return this._keys.length;
};

/**
 * Generate a string representation of this hash table.
 * @return {string} The resulting string.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.HashTable.prototype.toString = function() {
	try {
		var string = "";
		this.each(function(item, key) {
			if(string.length > 0) {
				string += "; ";
			}
			string += key + ": " + item.toString();
		});
		return string;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.HashTable::toString");
		console.log(e);
		return this.getMessage();
	}
};

/**
 * Get an array of items from this hash table where the keys match the given regular expression.
 * @param {RegExp} filter The regular expression to test keys against.
 * @return {array} The matching set of items.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.HashTable.prototype.filter = function(filter) {
	try {
		var result = [];
		this.each(function(item, key) {
			if(filter.test(key)) {
				result.push(item);
			}
		});
		return result;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.HashTable::filter");
		console.log(e);
		throw e;
	}
};

/**
 * Unload everything from the hash table.
 * The hash table should remain usable after calling this method.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.HashTable.prototype.unload = function() {
	try {
		var noBuckets = this._buckets.length;
		for(var i = 0; i < noBuckets; i++) {
			var bucket = this._buckets[i];
			if(typeof bucket == "object") {
				var noEntries = bucket.length;
				for(var j = 0; j < noEntries; j++) {
					var entry = bucket[j];
					entry.unload();
					bucket[j] = null;
				}
				// Reset the bucket
				this._buckets[i] = [];
			}
		}
		// Reset the array of keys
		this._keys = [];
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.HashTable::unload");
		console.log(e);
		throw e;
	}
	
};

/**
 * Get the bucket for the given key.
 * @private
 * @param {object} n The key.
 * @return The bucket the item with the given name would be in.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.HashTable.prototype._getBucket = function(n) {
	try {
		var hash = this.callHashFunction(n);
		if(hash >= this._buckets.length || hash < 0) {
			throw new Error("Hash is outside of allowed range.");
		}
		// if the bucket is undefined, initialise it to an empty array.
		if(!this._buckets[hash]) {
			this._buckets[hash] = [];
		}
		return this._buckets[hash];
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.HashTable::_getBucket");
		console.log(e);
		throw e;
	}
};

/**
 * @class An entry in a SitePoint.HashTable.
 * @constructor
 * @private
 * @param {object} key The key of the entry.
 * @param {object} value The value this entry refers to.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.HashTable.Entry = function(key, value) {
	this._key = key;
	this._value = value;
};

/**
 * @property {object} _key The key of this hash table entry.
 * @private
 */
SitePoint.HashTable.Entry.prototype._key = null;

/**
 * @property {object} _value The value of this hash table entry.
 * @private
 */
SitePoint.HashTable.Entry.prototype._value = null;

/**
 * Get the key of this entry.
 * @private
 * @return {object} The key of this entry.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.HashTable.Entry.prototype.getKey = function() {
	return this._key;
};

/**
 * Get the value this entry refers to
 * @private
 * @return {object} The value this entry refers to
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.HashTable.Entry.prototype.getValue = function() {
	return this._value;
};

/**
 * Unload this entry from memory. Calls the unload method of the underlying 
 * value if it exists, as it is expected that this will be accepeted as a
 * standard destructor method name.
 * @private
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.HashTable.Entry.prototype.unload = function() {
	try {
		var value = this.getValue();
		if(typeof value == "object" && value !== null) {
			if(typeof value.unload == "function") {
				try {
					value.unload();
				}
				catch(e) {
					console.log("An unhandled error occurred in a value's unload method.");
					console.log(e);
					console.log("Supressing this error.");
				}
			}
		}
		this._value = null;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.HashTable.Entry::unload");
		console.log(e);
		throw e;
	}
};
/**
 * @fileOverview
 * A class for cleaning up HTML. By default, all markup will be removed. It
 * is up to the user to specify which elements, attributes and other features
 * are allowed.
 * @name SitePoint.Html.Cleaner
 * @author Craig Anderson craig@sitepoint.com
 */

/**
 * The SitePoint namespace.
 * @namespace
 * @ignore
 */
var SitePoint = SitePoint || {};

/**
 * The SitePoint.Html namespace.
 * @namespace
 */
SitePoint.Html = SitePoint.Html || {};

/**
 * Create a cleaner.
 * @param {object} [allowedTagNames] A tag name which is allowed. Can also be an array of allowed tag names.
 * @param {object} [allowedAttributeNames] An attribute name which is allowed. Can also be an array of allowed attribute names.
 * @param {boolean} [allowComments] Whether comments are allowed. False by default.
 * @param {boolean} [allowProcesingInstructions] Whether processing instructions are allowed. False by default.
 * @param {boolean} [allowNamespaces] Whether namespaces are allowed. False by default.
 * @author Craig Anderson <craig@sitepoint.com>
 * @constructor
 */
SitePoint.Html.Cleaner = function(allowedTagNames, allowedAttributeNames, allowComments, allowProcessingInstructions, allowNamespaces) {
	try {
		// Initialise class members
		this._allowedTagNames = new SitePoint.Set();
		this._allowedAttributeNames = new SitePoint.Set();
		this._allowComments = false;
		this._startTagReplacementFunctions = new SitePoint.HashTable();
		this._endTagReplacementFunctions = new SitePoint.HashTable();
		this._hackFunctions = new SitePoint.HashTable();
		
		// Set values
		if(typeof allowedTagNames != "undefined") {
			this.addAllowedTagName(allowedTagNames);
		}
		if(typeof allowedAttributeNames != "undefined") {
			this.addAllowedAttributeName(allowedAttributeNames);
		}
		this.setAllowComments(allowComments);
		this.setAllowProcessingInstructions(allowProcessingInstructions);
		this.setAllowNamespaces(allowNamespaces);
		
		// Register the remove nested paragraph tags hack
		this.registerHack(function(html) {
			var paragraphDepth = 0;
			return html.replace(/<\/?p[^>]*>/gim, function(tag) {
				var replacement = tag;
				if(/<\//.test(tag)) {
					// end tag
					if(paragraphDepth > 1) {
						replacement = "";
					}
					paragraphDepth--;
				}
				else {
					// start tag
					paragraphDepth++;
					if(paragraphDepth > 1) {
						replacement = "";
					}
				}
				return replacement;
			});
		}, "Remove nested paragraph tags");
	}
	catch(e) {
		console.log("An unhandled error occurred in the SitePoint.Html.Cleaner constructor");
		console.log(e);
		throw e;
	}
};

/**
 * @property {array} _allowedTagNames An array of allowed tag names.
 * @private
 */
SitePoint.Html.Cleaner.prototype._allowedTagNames = null;

/**
 * @property {array} _allowedAttributeNames An array of allowed attribute names.
 * @private
 */
SitePoint.Html.Cleaner.prototype._allowedAttributeNames = null;

/**
 * @property {boolean} _allowComments Whether comments are allowed.
 * @private
 */
SitePoint.Html.Cleaner.prototype._allowComments = null;

/**
 * @property {boolean} _allowProcessingInstructions Whether processing instructions are allowed.
 * @private
 */
SitePoint.Html.Cleaner.prototype._allowProcessingInstructions = null;

/**
 * @property {boolean} _allowNamespaces Whether namespaces are allowed.
 * @private
 */
SitePoint.Html.Cleaner.prototype._allowNamespaces = null;

/**
 * @property {SitePoint.HashTable} _startTagReplacementFunctions Registered start tag replacement functions.
 * @private
 */
SitePoint.Html.Cleaner.prototype._startTagReplacementFunctions = null;

/**
 * @property {SitePoint.HashTable} _endTagReplacementFunctions Registered end tag replacement functions.
 * @private
 */
SitePoint.Html.Cleaner.prototype._endTagReplacementFunctions = null;

/**
 * @property {SitePoint.HashTable} _hackFunctions Registered hack functions.
 * @private
 */
SitePoint.Html.Cleaner.prototype._hackFunctions = null;

/**
 * Add an allowed tag name.
 * @param {object} tagName Either a tag name to allow, or an array of tag names to allow.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Html.Cleaner.prototype.addAllowedTagName = function(tagName) {
	try {
		if(typeof tagName == "string") {
			this._allowedTagNames.add(tagName.toLowerCase());
		}
		else if(SitePoint.Type.IsArray(tagName)) {
			var noTagNames = tagName.length;
			for(var i = 0; i < noTagNames; i++) {
				var allowedTagName = tagName[i];
				this.addAllowedTagName(allowedTagName);
			}
		}
		else {
			throw new TypeError("Invalid parameter");
		}
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Cleaner::addAllowedTagName");
		console.log(e);
		throw e;
	}
};

/**
 * Add an allowed attribute name.
 * @param {object} attributeName Either an attribute name to allow, or an array of attribute names to allow.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Html.Cleaner.prototype.addAllowedAttributeName = function(attributeName) {
	try {
		if(typeof attributeName == "string") {
			this._allowedAttributeNames.add(attributeName.toLowerCase());
		}
		else if(SitePoint.Type.IsArray(attributeName)) {
			var noAttributeNames = attributeName.length;
			for(var i = 0; i < noAttributeNames; i++) {
				var allowedAttributeName = attributeName[i];
				this.addAllowedAttributeName(allowedAttributeName);
			}
		}
		else {
			throw new TypeError("Invalid parameter");
		}
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Cleaner::addAllowedAttributeName");
		console.log(e);
		throw e;
	}
};

/**
 * Set whether to allow comments.
 * @param {boolean} allowComments
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Html.Cleaner.prototype.setAllowComments = function(allowComments) {
	this._allowComments = (allowComments == true); // make sure this value stays a boolean!
};

/**
 * Set whether to allow processing instructions.
 * @param {boolean} allowProcessingInstructions
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Html.Cleaner.prototype.setAllowProcessingInstructions = function(allowProcessingInstructions) {
	this._allowProcessingInstructions = (allowProcessingInstructions == true); // make sure this value stays a boolean!
};

/**
 * Set whether to allow namespaces.
 * @param {boolean} allowNamespaces
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Html.Cleaner.prototype.setAllowNamespaces = function(allowNamespaces) {
	this._allowNamespaces = (allowNamespaces == true); // make sure this value stays a boolean!
};

/**
 * Register a tag replacement function. Note that these functions will be called after the rest of the cleaning process has executed, excluding the hack functions.
 * @param {string} tagName The tag name being replaced. If a tag replacement function is already registered for this tag name, it will be replaced.
 * @param {function} replacementFunction The replacement function. This should accept parameters as per the a callback called by String.prototype.replace.
 * @param {boolean} [mode] One of 'start', 'end', or 'both'. 'both' by default.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Html.Cleaner.prototype.registerTagReplacementFunction = function(tagName, replacementFunction, mode) {
	// enforce default value of mode
	if(typeof mode == 'undefined') {
		mode = 'both';
	}
	switch(mode.toLowerCase()) {
		case 'start':
			this._startTagReplacementFunctions.add(tagName, replacementFunction);
			break;
		case 'end':
			this._endTagReplacementFunctions.add(tagName, replacementFunction);
			break;
		case 'both':
			this._startTagReplacementFunctions.add(tagName, replacementFunction);
			this._endTagReplacementFunctions.add(tagName, replacementFunction);
			break;
		default:
			throw new Error("Unrecognised mode");
			break;
	}
};

/**
 * Register a hack function. Note that these functions will be called after the rest of the cleaning process has executed.
 * @param {function} hackFunction The hack function. This function should accept a single parameter, which is the entire HTML string which has been cleaned, and return a replacement for it.
 * @param {string} [hackName] An optional name for this hack.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Html.Cleaner.prototype.registerHack = function(hackFunction, hackName) {
	if(typeof hackName == "undefined") {
		hackName = "Unnamed hack #" + this._hackFunctions.count() + 1;
	}
	this._hackFunctions.add(hackName, hackFunction);
};

/**
 * Remove a hack function.
 * @param {string} hackName The name of the hack to remove.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Html.Cleaner.prototype.removeHack = function(hackName) {
	this._hackFunctions.remove(hackName);
};

/**
 * Clean the given HTML string.
 * @param {string} html The HTML string to clean.
 * @return {string} The cleaned HTML.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Html.Cleaner.prototype.clean = function(html) {
	try {
		if(this._allowNamespaces) {
			// Find namespaces used in this document
			var namespaces = SitePoint.Html.Cleaner.ExtractAllNamespacePrefixes(html);
			// For each allowed tag, also allow it in each namespace
			var self = this;
			this._allowedTagNames.each(function(tagName) {
				var noNamespaces = namespaces.length;
				for(var namespaceIndex = 0; namespaceIndex < noNamespaces; namespaceIndex++) {
					var namespace = namespaces[namespaceIndex];
					self.addAllowedTagName(namespace + ":" + tagName);
				}
			});
			self = null;
			// Add allowed attribute for each namespace's declaration
			var noNamespace = namespaces.length;
			for(var namespaceIndex = 0; namespaceIndex < noNamespace; namespaceIndex++) {
				var namespace = namespaces[namespaceIndex];
				this.addAllowedAttributeName("xmlns:" + namespace);
			}
			// Add allowed attribute for default namespace's declaration
			this.addAllowedAttributeName("xmlns");
		}
		else {
			html = SitePoint.Html.Cleaner.RemoveNamespacePrefixesFromTags(html);
		}
		
		// Remove tags
		var allTagNames = new SitePoint.Set(SitePoint.Html.Cleaner.ExtractAllTagNames(html));
		var tagNames = allTagNames.complement(this._allowedTagNames).toArray();
		var noTagNames = tagNames.length;
		for(var tagNameIndex = 0; tagNameIndex < noTagNames; tagNameIndex++) {
			var tagName = tagNames[tagNameIndex];
			html = SitePoint.Html.Cleaner.RemoveTags(tagName, html);
		}
		
		// Remove attributes
		var allAttributeNames = new SitePoint.Set(SitePoint.Html.Cleaner.ExtractAllAttributeNames(html));
		var attributeNames = allAttributeNames.complement(this._allowedAttributeNames).toArray();
		SitePoint.Arrays.Sort(attributeNames, function(a, b) {
			return (a.length - b.length);
		});
		var noAttributeNames = attributeNames.length;
		for(var attributeNameIndex = 0; attributeNameIndex < noAttributeNames; attributeNameIndex++) {
			var attributeName = attributeNames[attributeNameIndex];
			html = SitePoint.Html.Cleaner.RemoveAttributes(attributeName, html);
		}
		
		// Remove comments
		if(!this._allowComments) {
			html = SitePoint.Html.Cleaner.RemoveComments(html);
		}
		
		// Remove processing instructions
		if(!this._allowProcessingInstructions) {
			html = SitePoint.Html.Cleaner.RemoveProcessingInstructions(html);
		}
		
		// Call start tag replacement functions
		tagNames = this._startTagReplacementFunctions.getKeys();
		noTagNames = tagNames.length;
		for(var tagNameIndex = 0; tagNameIndex < noTagNames; tagNameIndex++) {
			var tagName = tagNames[tagNameIndex];
			var replacementFunction = this._startTagReplacementFunctions.get(tagName);
			var startTagRegExp = new RegExp("<" + tagName + "\\s?[^>]*>", "gim");
			html = html.replace(startTagRegExp, replacementFunction);
		}
		
		// Call end tag replacement functions
		tagNames = this._endTagReplacementFunctions.getKeys();
		noTagNames = tagNames.length;
		for(var tagNameIndex = 0; tagNameIndex < noTagNames; tagNameIndex++) {
			var tagName = tagNames[tagNameIndex];
			var replacementFunction = this._endTagReplacementFunctions.get(tagName);
			var endTagRegExp = new RegExp("<\\/" + tagName + ">", "gim");
			html = html.replace(endTagRegExp, replacementFunction);
		}
		
		// Call hack functions
		this._hackFunctions.each(function(hackFunction, hackName) {
			try {
				html = hackFunction(html);
			}
			catch(e) {
				console.log("An unhandled error occurred in hack function \"" + hackName + "\".");
				console.log(e);
				throw e;
			}
		});
		
		return html;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Cleaner::clean");
		console.log(e);
		throw e;
	}
};

/**
 * Remove any instances of the given attibute from the given HTML string.
 * A known issue with this method where removing an attribute 'align' will
 * also remove some of an attribute called 'valign', leaving only the 'v'.
 * @param {string} attributeName The name of the attribute to remove.
 * @param {string} html The HTML to remove the attribute from.
 * @return {string} The cleaned HTML.
 * @author Craig Anderson <craig@sitepoint.com>
 * @static
 */
SitePoint.Html.Cleaner.RemoveAttributes = function(attributeName, html) {
	try {
		// Find and remove any attributes with double-quoted values
		var regex = new RegExp("" + attributeName + "=\"[^\"]*\"", "gim");
		html = html.replace(regex, "");
		// Find and remove any attributes with single-quoted values
		regex = new RegExp("" + attributeName + "='[^']*'", "gim");
		html = html.replace(regex, "");
		// Find and remove any attributes with non-quoted values
		regex = new RegExp("" + attributeName + "=[^\\s>]*", "gim");
		html = html.replace(regex, "");
		// Find and remove any attributes with no values
		regex = new RegExp("(<[^>]*)(" + attributeName + ")([^:a-z0-9])([^>]*>)", "gim");
		html = html.replace(regex, "$1$3$4");
		// Return the result
		return html;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Cleaner.RemoveAttributes");
		console.log(e);
		throw e;
	}
};

/**
 * Remove any instances of the given element's tags from the given HTML string.
 * @example print(SitePoint.Html.Cleaner.prototypeRemoveTags("p", "<p>foobar</p>")); // prints "foobar"
 * @param {string} tagName The tag name of the element to remove.
 * @param {string} html The HTML to remove the elements from.
 * @return {string} The cleaned HTML.
 * @author Craig Anderson <craig@sitepoint.com>
 * @static
 */
SitePoint.Html.Cleaner.RemoveTags = function(tagName, html) {
	try {
		// Remove start tags and empty tags
		var regex = new RegExp("<" + tagName + "(\\s+[^>]*)?>", "gim");
		html = html.replace(regex, "");
		// Remove end tags
		regex = new RegExp("</" + tagName + ">", "gim");
		html = html.replace(regex, "");
		// Return the result
		return html;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Cleaner.RemoveTags");
		console.log(e);
		throw e;
	}
};

/**
 * Remove comments from the given HTML string
 * @param {string} html The HTML string to remove comments from.
 * @return {string} The HTML string with the comments removed.
 * @author Craig Anderson <craig@sitepoint.com>
 * @static
 */
SitePoint.Html.Cleaner.RemoveComments = function(html) {
	try {
		// HTML is "foo<!-- a comment -->bar"
		var substrings = html.split("<!--");
		// substrings[0] = "foo"
		// substrings[1] = " a comment -->bar"
		var result = substrings[0];
		var noSubstrings = substrings.length;
		for(var substringIndex = 1; substringIndex < noSubstrings; substringIndex++) {
			// substring = " a comment -->bar"
			var substring = substrings[substringIndex];
			// Split this substring by the end of comment delimiter
			var subsubstrings = substring.split("-->");
			// subsubstrings[0] = " a comment "
			// subsubstrings[1] = "bar"
			// Verify that this split the substring into two parts
			if(subsubstrings.length != 2) {
				throw new Error("Could not find the end of a comment");
			}
			result += subsubstrings[1];
		}
		return result;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Cleaner.RemoveComments");
		console.log(e);
		throw e;
	}
};

/**
 * Remove XML processing instructions from the given HTML string. This
 * includes badly formed processing instructions in the form "<? some-pi />".
 * @param {string} html The HTML string to clean.
 * @return {string} The cleaned HTML string.
 * @author Craig Anderson <craig@sitepoint.com>
 * @static
 */
SitePoint.Html.Cleaner.RemoveProcessingInstructions = function(html) {
	try {
		// HTML is "foo<? some-processing-instruction ?>bar"
		var substrings = html.split("<?");
		// substrings[0] = "foo"
		// substrings[1] = " some-processing-instruction ?>bar"
		var result = substrings[0];
		var noSubstrings = substrings.length;
		for(var substringIndex = 1; substringIndex < noSubstrings; substringIndex++) {
			// substring = " some-processing-instruction ?>bar"
			var substring = substrings[substringIndex];
			// Split this substring by the end of processing instruction delimiter
			// Note that this can be '?>' or '/>' (according to Microsoft Word, at least)
			var subsubstrings = substring.split(/[\?|\/]>/);
			// subsubstrings[0] = " some-processing-instruction "
			// subsubstrings[1] = "bar"
			// Verify that this split the substring into two parts
			if(subsubstrings.length != 2) {
				throw new Error("Could not find the end of a processing instruction");
			}
			result += subsubstrings[1];
		}
		return result;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Cleaner.RemoveProcessingInstructions");
		console.log(e);
		throw e;
	}
};

/**
 * Remove the namespace prefix from all tags in the given HTML string.
 * @param {string} html
 * @return {string} The cleaned HTML.
 * @author Craig Anderson <craig@sitepoint.com>
 * @static
 */
SitePoint.Html.Cleaner.RemoveNamespacePrefixesFromTags = function(html) {
	try {
		// Remove prefixes from start tags
		var regex = /<[a-z0-9]+:([a-z0-9])+/gim;
		html = html.replace(regex, "<$1");
		// Remove prefixes from end tags
		regex = /<\/[a-z0-9]+:([a-z0-9])+/gim;
		html = html.replace(regex, "</$1");
		return html;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Cleaner.RemoveTags");
		console.log(e);
		throw e;
	}
};


/**
 * Extract all tag names from the given string of HTML
 * @param {string} html The string of HTML to extract tags names from
 * @return {array} An array of tag names.
 * @author Craig Anderson <craig@sitepoint.com>
 * @static
 */
SitePoint.Html.Cleaner.ExtractAllTagNames = function(html) {
	try {
		var tagNames = [];
		var match = null;
		var tagNameRegex = /<\/?([a-z0-9:]+)/gi;
		while(match = tagNameRegex.exec(html)) {
			tagNames.push(match[1].toLowerCase());
		}
		SitePoint.Arrays.RemoveDuplicates(tagNames);
		return tagNames;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Cleaner.ExtractTagNames");
		console.log(e);
		throw e;
	}
};

/**
 * Extract all attribute names from the given string of HTML
 * @param {string} html The string of HTML to extract attributes names from
 * @return {array} An array of attribute names.
 * @author Craig Anderson <craig@sitepoint.com>
 * @static
 */
SitePoint.Html.Cleaner.ExtractAllAttributeNames = function(html) {
	try {
		var attributeNames = [];
		var match = null;
		var tagRegex = /<([a-z0-9][^>]*)>/gi;
		while(match = tagRegex.exec(html)) {
			var tag = match[1];
			// remove quoted values from the tag
			tag = tag.replace(/"[^"]*"/g, "");
			tag = tag.replace(/'[^']*'/g, "");
			var attributeRegex = /\s+([^=\s]+)/gi;
			while(match = attributeRegex.exec(tag)) {
				attributeNames.push(match[1].toLowerCase());
			}
		}
		SitePoint.Arrays.RemoveDuplicates(attributeNames);
		return attributeNames;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Cleaner.ExtractAllAttributeNames");
		console.log(e);
		throw e;
	}
};

/**
 * Extract all namespace prefixes from the given string of HTML.
 * @param {string} html The HTML to extract namespace prefixes from.
 * @return {array} The namespaces.
 * @author Craig Anderson <craig@sitepoint.com>
 * @static
 */
SitePoint.Html.Cleaner.ExtractAllNamespacePrefixes = function(html) {
	try {
		var namespaces = new SitePoint.Set();
		var tagNames = SitePoint.Html.Cleaner.ExtractAllTagNames(html);
		var noTagNames = tagNames.length;
		for(var tagNameIndex = 0; tagNameIndex < noTagNames; tagNameIndex++) {
			var tagName = tagNames[tagNameIndex];
			var match = null;
			if(match = /^([a-z0-9]+):/gim.exec(tagName)) {
				// found a namespace
				namespaces.add(match[1].toLowerCase());
			}
		}
		var attributeNames = SitePoint.Html.Cleaner.ExtractAllAttributeNames(html);
		var noAttributeNames = attributeNames.length;
		for(var attributeNameIndex = 0; attributeNameIndex < noAttributeNames; attributeNameIndex++) {
			var attributeName = attributeNames[attributeNameIndex];
			var match = null;
			if(match = /^xmlns:([a-z0-9]+)$/gi.exec(attributeName)) {
				// found a namespace
				namespaces.add(match[1].toLowerCase());
			}
		}
		return namespaces.toArray();
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Cleaner.ExtractAllNamespacePrefixes");
		console.log(e);
		throw e;
	}
};
/**
 * @fileOverview
 * A class for comparing strings of HTML. This class can "sort-of" compare
 * HTML elements (or an HTML string with an HTML element), but this support
 * is incomplete and not ready for production use. This class requires
 * SitePoint.HashTable and SitePoint.Type.
 * @name SitePoint.Html.Comparer
 * @author Craig Anderson craig@sitepoint.com
 */

/**
 * The SitePoint namespace
 * @namespace
 * @ignore
 */
var SitePoint = SitePoint || {};

/**
 * The SitePoint.Html namespace
 * @namespace
 * @ignore
 */
SitePoint.Html = SitePoint.Html || {};

// Create Firebug stub if required
// Note that we should only use Firebug functions listed below.
window.console = window.console || { log: function() { /* do nothing */ } };

/**
 * A class for comparing strings of HTML.
 * @param {SitePoint.HashTable} options Options for this comparer.
 * @author Craig Anderson <craig@sitepoint.com>
 * @constructor
 */
SitePoint.Html.Comparer = function(options) {
	if(typeof options == "object" &&
			typeof options.get == "function") {
		this.options = options;
	}
	else {
		this.options = new SitePoint.HashTable();
		this.options.add("caseSensitive", true);
		this.options.add("nonBreakingSpaceIsWhitespace", false);
		this.options.add("ignoreInsignificantWhitespace", true);
		this.options.add("ignoreWhitespace", false);
		this.options.add("markupCaseSensitive", false);
		this.options.add("ignoreEmptyElements", false);
	}
};

/**
 * @property {SitePoint.HashTable} options The options to be used in this object's comparisons.
 * @example var comparer = new SitePoint.Html.Comparer();
 * comparer.options.add("caseSensitive", true);                 // makes comparisons case-sensitive
 * comparer.options.add("markupCaseSensitive", true);           // makes comparisons of tags names and attributes case-sensitive
 * comparer.options.add("ignoreWhitespace", true);              // ignore all whitespace in comparisons
 * comparer.options.add("ignoreInsignificantWhitespace", true); // ignore only insignificant whitespace
 * comparer.options.add("nonBreakingSpaceIsWhitespace", true);  // include &amp;nbsp; as whitespace
 * comparer.options.add("ignoreEmptyElements", true);           // ignore empty elements such as &lt;br&gt;, &lt;img&gt;, &lt;span&gt;&lt;/span&gt;, etc.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Html.Comparer.prototype.options = null;

/**
 * Compare two given pieces of HTML.
 * @param {string} a The first piece of HTML to compare.
 * @param {string} b The second piece of HTML to compare.
 * @return {boolean} Whether the pieces were deemed equal or not.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Html.Comparer.prototype.compare = function(a, b) {
	try {
		// Check if paramters are strings or HTML elements
		a = this._normaliseParameter(a);
		b = this._normaliseParameter(b);
		
		// Non-breaking space is whitespace
		if(this.options.get("nonBreakingSpaceIsWhitespace")) {
			a = a.replace(/&nbsp;/, " ");
			b = b.replace(/&nbsp;/, " ");
		}
		// Ignore insignificant whitespace
		if(this.options.get("ignoreInsignificantWhitespace")) {
			// Start of string
			a = a.replace(/^\s*/, "");
			b = b.replace(/^\s*/, "");
			// Between tags
			a = a.replace(/>\s*</g, "><");
			b = b.replace(/>\s*</g, "><");
			// End of string
			a = a.replace(/\s*$/, "");
			b = b.replace(/\s*$/, "");
		}
		// Ignore all whitespace
		if(this.options.get("ignoreWhitespace")) {
			a = a.replace(/\s/g, "");
			b = b.replace(/\s/g, "");
		}
		// Case sensitvity
		if(!this.options.get("caseSensitive")) {
			a = a.toUpperCase();
			b = b.toUpperCase();
		}
		// Markup case sensitivity
		if(!this.options.get("markupCaseSensitive")) {
			a = a.replace(/<[^>]*>/g, function(x) { return x.toUpperCase(); });
			b = b.replace(/<[^>]*>/g, function(x) { return x.toUpperCase(); });
		}
		// Empty elements
		if(this.options.get("ignoreEmptyElements")) {
			// Remove empty elements in the form <tag ...></tag>
			a = a.replace(/<(\w+)[^>]*><\/\1>/g, "");
			b = b.replace(/<(\w+)[^>]*><\/\1>/g, "");
			// Remove empty elements in the form <tag ... />
			a = a.replace(/<[^>]*\/>/g, "");
			b = b.replace(/<[^>]*\/>/g, "");
		}
		return (a == b);
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Comparer::compare");
		console.log(e);
		throw e;
	}
};

/**
 * Convert a parameter passed into compare to a string, or throw a TypeError
 * if the parameter is an invalid type.
 * Note that conversion from an HTML element to a string is currently
 * incomplete and should not be used for anything serious. I plan to write
 * a SitePoint.Html.Serialiser class which will facilitate such a conversion.
 * @param {object} x The parameter to compare.
 * @return {string} The normalised parameter.
 * @author Craig Anderson <craig@sitepoint.com>
 * @private
 */
SitePoint.Html.Comparer.prototype._normaliseParameter = function(x) {
	try {
		var result;
		if(typeof x == "string") {
			result = x;
		}
		else if(SitePoint.Type.IsHtmlElement(x)) {
			// x is an HTML element
			if(typeof x.outerHTML == "string") {
				result = x.outerHTML;
			}
			else {
				// TODO: Introduce some better outerHTML alternative
				result = "";
				result += "<" + x.tagName + ">";
				result += x.innerHTML;
				result += "</" + x.tagName + ">";
			}
		}
		else {
			throw new TypeError("This parameter is not a string or an HTML element, so it cannot be used for comparison.");
		}
		return result;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Comparer::_normaliseParameter");
		console.log(e);
		throw e;
	}
};
/**
 * @fileOverview
 * A collection of methods for dealing with HTML tables in the DOM. Requires
 * SitePoint.Type.
 * @name SitePoint.Html.Dom.Tables
 * @author Craig Anderson <craig@sitepoint.com>
 */

/**
 * The SitePoint namespace.
 * @namespace
 * @ignore
 */
var SitePoint = SitePoint || {};

/**
 * The SitePoint.Html namespace.
 * @namespace
 * @ignore
 */
SitePoint.Html = SitePoint.Html || {};

/**
 * The SitePoint.Html.Dom namespace.
 * @namespace
 * @ignore
 */
SitePoint.Html.Dom = SitePoint.Html.Dom || {};


/**
 * Namespace for table DOM functions.
 * @namespace
 */
SitePoint.Html.Dom.Tables = SitePoint.Html.Dom.Tables || {};

// firebug stub
window.console = window.console || { log: function() { /* do nothing */ } };

/**
 * Get the number of rows in a table.
 * @param {HTMLTableElement} table The table.
 * @return {integer} The number of rows in the table.
 * @author Craig Anderson <craig@sitepoint.com>
 * @static
 */
SitePoint.Html.Dom.Tables.NoOfRows = function(table) {
	try {
		return table.rows.length;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Dom.Tables.NoOfColumns");
		console.log(e);
		throw e;
	}
};

/**
 * Get the number of columns in a table.
 * @param {HTMLTableElement} table The table.
 * @return {integer} The number of columns in the table. 0 if no cells were found.
 * @author Craig Anderson <craig@sitepoint.com>
 * @static
 */
SitePoint.Html.Dom.Tables.NoOfColumns = function(table) {
	try {
		// declare array to store the number of columns in each row
		var noCellsInRows = [];
		// initialise each row
		var noRows = SitePoint.Html.Dom.Tables.NoOfRows(table);
		for(var rowIndex = 0; rowIndex < noRows; rowIndex++) {
			noCellsInRows[rowIndex] = 0;
		}
		// count the number of columns in each row, including colspans and rowspans
		SitePoint.Html.Dom.Tables.ForEachCell(table, function(cell, coords) {
			// get the colspan for this cell. 
			var colspan = parseInt($(cell).attr("colspan"));
			if(isNaN(colspan)) {
				colspan = 1;
			}
			var row = coords[0];
			var cellsInRow = noCellsInRows[row];
			cellsInRow = cellsInRow + colspan;
			noCellsInRows[row] = cellsInRow;
		});
		var maxNoCells = 0;
		for(var i = 0; i < noRows; i++) {
			if(noCellsInRows[i] > maxNoCells) {
				maxNoCells = noCellsInRows[i];
			}
		}
		return maxNoCells;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Dom.Tables.NoOfColumns");
		console.log(e);
		throw e;
	}
};


/**
 * Call the given function on each cell in a table.
 * @param {HTMLTableElement} table The table.
 * @param {function} f The function to call. This function will be passed two parameters: the cell, and the coordinates of the cell in [row, cell] format.
 * @author Craig Anderson <craig@sitepoint.com>
 * @static
 */
SitePoint.Html.Dom.Tables.ForEachCell = function(table, f) {
	try {
		var rows = table.rows;
		var noRows = rows.length;
		for(var rowIndex = 0; rowIndex < noRows; rowIndex++) {
			var row = rows[rowIndex];
			var cells = row.cells;
			var noCells = cells.length;
			for(var cellIndex = 0; cellIndex < noCells; cellIndex++) {
				var cell = cells[cellIndex];
				try {
					f(cell, [rowIndex, cellIndex]);
				}
				catch(e) {
					console.log("An unhandled error occurred calling the function on the cell at row #" + (rowIndex + 1) + ", cell #" + (cellIndex + 1));
					console.log(e);
					throw e;
				}
			}
		}
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Dom.Tables.ForEachCell");
		console.log(e);
		throw e;
	}
};

/**
 * Call the given function on each cell in a particular column in a table.
 * @param {HTMLTableElement} table The table.
 * @param {integer} columnIndex The column index. 0 is the first column, 1 is the second, etc.
 * @author Craig Anderson <craig@sitepoint.com>
 * @static
 */
SitePoint.Html.Dom.Tables.ForEachCellInColumn = function(table, columnIndex, f) {
	try {
		if(columnIndex >= SitePoint.Html.Dom.Tables.NoOfColumns(table)) {
			throw new Error("Supplied column index exceeds the number of columns in this table");
		}
		SitePoint.Html.Dom.Tables.ForEachCell(table, function(cell, coords) {
			thisColumnIndex = coords[1];
			if(thisColumnIndex == columnIndex) {
				f(cell, coords[0]);
			}
		});
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Dom.Tables.ForEachCellInColumn");
		console.log(e);
		throw e;
	}
};

/**
 * Redistribute column widths in the given table.
 * @param {HTMLTableElement} table The table.
 * @param {Array} [widestValues] An array of the widest expected values for each column.
 * @author Craig Anderson <craig@sitepoint.com>
 * @static
 */
SitePoint.Html.Dom.Tables.RedistributeColumnWidths = function(table, widestValues) {
	try {
		// Ensure widestValues is an array
		if(!SitePoint.Type.IsArray(widestValues)) {
			widestValues = [];
		}
		
		// Make sure widestValues has the same number of entries as there are columns in this table
		var noColumns = SitePoint.Html.Dom.Tables.NoOfColumns(table);
		while(widestValues.length < noColumns) {
			widestValues.push("");
		}
		
		// Get the original width of the table. We will not change this width.
		var originalWidth = $(table).width();
		
		// Append the text displayed in each form control to the form control,
		// then hide the form control.
		SitePoint.Html.Dom.Tables.ForEachCell(table, function(cell, coords) {
			// text fields
			$("input[@type='text']", cell).each(function() {
				// append the value of this text field to the cell
				var val = $(this).val();
				$(cell).append("<span class='SitePoint_Html_Dom_Tables_RedistributeColumnWidths_Placeholder'>" + val + "</span>");
				// hide the text field
				$(this).hide();
				$(this).addClass("SitePoint_Html_Dom_Tables_RedistributeColumnWidths_Hidden");
			});
			// select boxes
			$("select", cell).each(function() {
				// get the longest string for an option in this drop-down list
				var longestString = "";
				$("option", this).each(function() {
					var thisString = this.innerHTML;
					if(thisString.length > longestString.length) {
						longestString = thisString;
					}
				});
				// append the longest string to the cell
				$(cell).append("<span class='SitePoint_Html_Dom_Tables_RedistributeColumnWidths_Placeholder'>" + longestString + "</span>");
				// hide the select field
				$(this).hide();
				$(this).addClass("SitePoint_Html_Dom_Tables_RedistributeColumnWidths_Hidden");
			});
		});
		
		// Create a temporary row in the table for the values in widestValues
		var tempRow = $("<tr class='SitePoint_Html_Dom_Tables_RedistributeColumnWidths_Placeholder'></tr>").get(0);
		$.each(widestValues, function(index, value) {
			var tempCell = $("<td>" + value + "</td>").get(0);
			$(tempRow).append(tempCell);
		});
		$(table).append(tempRow);
		
		// Get the current width of the table, and calculate the scaling factor
		var newWidth = $(table).width();
		var scalingFactor = originalWidth / newWidth;
		
		// Set the width of each cell
		SitePoint.Html.Dom.Tables.ForEachCell(table, function(cell, coords) {
			var newWidth = $(cell).width() * scalingFactor;
			
			$(cell).width(newWidth);
		});
		
		// Remove any added placeholders, and show any hidden fields
		$(".SitePoint_Html_Dom_Tables_RedistributeColumnWidths_Hidden").show();
		$(".SitePoint_Html_Dom_Tables_RedistributeColumnWidths_Placeholder").remove();
		$(".SitePoint_Html_Dom_Tables_RedistributeColumnWidths_Hidden").removeClass("SitePoint_Html_Dom_Tables_RedistributeColumnWidths_Hidden");
		
		// Hide and then show the table again.
		// This forces MSIE to repaint the table.
		$(table).hide();
		$(table).show();
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Dom.Tables.RedistributeColumnWidths");
		console.log(e);
		throw e;
	}
};

/**
 * Duplicate a column in a table.
 * @param {HTMLTableElement} table The table to duplicate a column in.
 * @param {integer} prototypeColumnIndex Index of the column to duplicate.
 * @param {integer} newColumnIndex What will be the index of the new column.
 * @param (function} cellAddedCallback A function which will be called as each cell is added.
 * @author Craig Anderson <craig@sitepoint.com>
 * @static
 */
SitePoint.Html.Dom.Tables.DuplicateColumn = function(table, prototypeColumnIndex, newColumnIndex, cellAddedCallback) {
	try {
		if(typeof cellAddedCallback != "function") {
			cellAddedCallback = function() {};
		}
		
		// iterate through each row in the table
		$("./tr", table).each(function(rowIndex, tr) {
			// check this row is part of the table we're iterating over and not
			// some other table inside our table
			var trTable = SitePoint.Html.Dom.Traversal.FindParent(tr, "table");
			if(trTable == table) {
				// clone the prototype cell
				var prototypeCell = $(tr).children().get(prototypeColumnIndex);
				if(!prototypeCell) {
					throw new Error("Could not find a prototype cell in row #" + rowIndex);
				}
				var newCell = $(prototypeCell).clone();

				// get the cell currently at the desired index and attempt to insert the new cell before it
				var insertBefore = true;
				var neighbourCell = $(tr).children().get(newColumnIndex);
				if(!neighbourCell) {
					// could not find a cell in that position, try and find a cell before it
					neighbourCell = $(tr).children().get(newColumnIndex - 1);
					if(!neighbourCell) {
						throw new Error("Could not find a neighbour cell in row #" + rowIndex);
					}
					insertBefore = false;
				}
				if(insertBefore) {
					$(neighbourCell).before(newCell);
				}
				else {
					$(neighbourCell).after(newCell);
				}
				cellAddedCallback(newCell);
			}
		});
		
		// duplicate the col element if one exists
		var prototypeColumn = $("col", table).get(prototypeColumnIndex);
		if(prototypeColumn) {
			var newColumn = $(prototypeColumn).clone(true);
			var insertBefore = true;
			var neighbourColumn = $("col", table).get(newColumnIndex);
			if(!neighbourColumn) {
				// could not find a cell in that position, try and find a cell before it
				neighbourColumn = $("col", table).get(newColumnIndex - 1);
				insertBefore = false;
			}
			if(neighbourColumn) {
				if(insertBefore) {
					$(neighbourColumn).before(newColumn);
				}
				else {
					$(neighbourColumn).after(newColumn);
				}
			}
		}
		
		// HACK: Hide and show table so firefox paints it correctly
		if($.browser.mozilla) {
			$(table).css("display", "none");
			setTimeout(function() {
				$(table).css("display", "table");
			}, 0);
		}
		
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Dom.Tables.DuplicateColumn");
		console.log(e);
		throw e;
	}
};

/**
 * Remove a column in a table.
 * @param {HTMLTableElement} table The table to remove the column from.
 * @param {integer} columnIndex The column index.
 * @author Craig Anderson <craig@sitepoint.com>
 * @static
 */
SitePoint.Html.Dom.Tables.RemoveColumn = function(table, columnIndex) {
	try {
		// iterate through each row in the table
		$("tr", table).each(function(rowIndex, tr) {
			var cell = $(tr).children().get(columnIndex);
			$(cell).remove();
		});
		// remove col element if one exists
		var col = $("col", table).get(columnIndex);
		if(col) {
			$(col).remove();
		}
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Dom.Tables.RemoveColumn");
		console.log(e);
		throw e;
	}
};
/**
 * @fileOverview
 * A collection of methods for traversing DOM documents.
 * @name SitePoint.Html.Dom.Traversal
 * @author Craig Anderson craig@sitepoint.com
 */

/**
 * The SitePoint namespace
 * @namespace
 * @ignore
 */
var SitePoint = SitePoint || {};

/**
 * The SitePoint.Html namespace
 * @namespace
 * @ignore
 */
SitePoint.Html = SitePoint.Html || {};

/**
 * The SitePoint.Html.Dom namespace
 * @namespace
 * @ignore
 */
SitePoint.Html.Dom = SitePoint.Html.Dom || {};

/**
 * The SitePoint.Html.Dom namespace
 * @namespace
 * @ignore
 */
SitePoint.Html.Dom.Traversal = SitePoint.Html.Dom.Traversal || {};



// Create Firebug stub if required
// Note that we should only use Firebug functions listed below.
window.console = window.console || { log: function() { /* do nothing */ } };

/**
 * Find a parent node of the specified type.
 * @param {HTMLElement} element The HTML element to search for parents of.
 * @param {string} tagName The tag name of the parent being searched for.
 * @param {string} [terminalTagName] The tag name which, when reached, will stop searching. Defaults to "HTML" if omitted.
 * @param {integer} [depth] The maximum depth to search to. 100 if omitted.
 * @return {HTMLElement} The HTML element, or null if none was found.
 * @author Craig Anderson <craig@sitepoint.com>
 * @static
 */
SitePoint.Html.Dom.Traversal.FindParent = function(element, tagName, terminalTagName, depth) {
	try {
		// set default values
		if(typeof terminalTagName == "undefined") {
			terminalTagName = "HTML";
		}
		if(typeof depth == "undefined") {
			depth = 100;
		}
		
		// check data types
		if(!SitePoint.Type.IsHtmlElement(element)) {
			throw new TypeError("element must be an HTML element.");
		}
		if(typeof tagName != "string") {
			throw new TypeError("tagName must be a string");
		}
		if(typeof terminalTagName != "string") {
			throw new TypeError("terminalTagName must be a string");
		}
		if(typeof depth != "number" || depth < 0) {
			throw new TypeError("depth must be a positive integer");
		}
		
		// massage data into appropriate format
		tagName = tagName.toUpperCase();
		terminalTagName = terminalTagName.toUpperCase();
		depth = parseInt(depth);
		
		var result = null;
		if(depth > 0) {
			depth--;
			var thisTagName = element.nodeName.toUpperCase();
			if(thisTagName == tagName) {
				result = element;
			}
			else if(thisTagName == terminalTagName) {
				// we're done; exit
				result = null;
			}
			else {
				var parentNode = element.parentNode;
				if(SitePoint.Type.IsHtmlElement(parentNode)) {
					result = SitePoint.Html.Dom.Traversal.FindParent(parentNode, tagName, terminalTagName, depth);
				}
			}
		}
		return result;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Dom.Traversal.FindParent");
		console.log(e);
		throw e;
	}
};
/**
 * @fileOverview
 * A class for parsing form controls (i.e. select, textarea and input
 * elements) out of HTML strings.
 * @name SitePoint.Html.Parsing.FormControls
 * @author Craig Anderson <craig@sitepoint.com>
 */


/**
 * The SitePoint namespace
 * @namespace
 * @ignore
 */
var SitePoint = SitePoint || {};

/**
 * The SitePoint.Html namespace
 * @namespace
 * @ignore
 */
SitePoint.Html = SitePoint.Html || {};

/**
 * The SitePoint.Html.Parsing namespace
 * @namespace
 * @ignore
 */
SitePoint.Html.Parsing = SitePoint.Html.Parsing || {};

/**
 * The SitePoint.Html.Parsing.FormControls namespace
 * @namespace
 */
SitePoint.Html.Parsing.FormControls = SitePoint.Html.Parsing.FormControls || {};

// Create Firebug stub if required
// Note that we should only use Firebug functions listed below.
window.console = window.console || { log: function() { /* do nothing */ } };


/**
 * Parse the given string of HTML for form controls.
 * @param {string} html The HTML to parse form controls from.
 * @param {boolean} [inOrder] Whether the elements are returned in the order they appear in the document or not. False is *much* faster. False by default.
 * @return {array} The form controls found in the HTML. Note that this array is not orderered.
 * @author Craig Anderson <craig@sitepoint.com>
 * @static
 */
SitePoint.Html.Parsing.FormControls.Parse = function(html, inOrder) {
	try {
		if(typeof inOrder == "undefined") {
			inOrder = false;
		}
		
		var formControls = [];
		// Create a temporary element. This will be used to instantiate our
		// elements by setting it's innerHTML.
		var tempElement = document.createElement("DIV");
		
		if(inOrder) {
			// instantiate the HTML
			tempElement.innerHTML = html;
			// iterate through eahc element in the HTML, recording any found INPUTs, SELECTs, and TEXTAREAs.
			formControls = SitePoint.Html.Parsing.FormControls._FindChildren(tempElement);
		}
		else {
			// Find input elements
			var inputRegExp = /<input[^>]*>/gi;
			while((match = inputRegExp.exec(html))) {
				tempElement.innerHTML = match[0];
				formControls.push(tempElement.childNodes[0]);
			}
			// Find select elements
			var selectRegExp = /<select[^>]*>(?:[^<]*<option[^>]*>[^<]*<\/option>)*[^<]*<\/select>/gi;
			while((match = selectRegExp.exec(html))) {
				// Create the element, and then create a clone of it. We need to
				// clone selects so its options don't get lost when tempElement
				// disappears.
				tempElement.innerHTML = match[0];
				var clone = tempElement.childNodes[0].cloneNode(true);
				// Need to reset selectedIndex on cloned list in MSIE
				clone.selectedIndex = tempElement.childNodes[0].selectedIndex;
				// Add the clone to the array of form controls
				formControls.push(clone);
			}
			// Find textarea elements
			// Find where each textarea starts
			var startIndices = [];
			var textareaStartRegExp = /<textarea[^>]*>/gi;
			while((match = textareaStartRegExp.exec(html))) {
				startIndices.push(match.index);
			}
			// Find where each textarea ends
			var endIndices = [];
			var textareaEndRegExp = /<\/textarea>/gi;
			while((match = textareaEndRegExp.exec(html))) {
				endIndices.push(match.index + match[0].length);
			}
			// Make sure we've got the same number of starts and finishes.
			if(startIndices.length != endIndices.length) {
				throw new Error("Could not find a matching number of <textarea> and </textarea> tags.");
			}
			// Now create each textarea
			var noTextareas = startIndices.length;
			for(var i = 0; i < noTextareas; i++) {
				var startIndex = startIndices[i];
				var endIndex = endIndices[i];
				var textareaHtml = html.slice(startIndex, endIndex);
				tempElement.innerHTML = textareaHtml;
				formControls.push(tempElement.childNodes[0].cloneNode(true));
			}
		}
		
		// Clean up
		tempElement = null;
		
		return formControls;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Parsing.FormControls.Parse");
		console.log(e);
		throw e;
	}
};

/**
 * Find form control children of this element.
 * @param {HTMLElement} element The HTML element to search for children of.
 * @param {integer} [depth] THe current depth. If omitted, defaults to 100.
 * @return {array} Form controls.
 * @author Craig Anderson <craig@sitepoint.com>
 * @private
 * @static
 */
SitePoint.Html.Parsing.FormControls._FindChildren = function(element, depth) {
	try {
		var formControls = [];
		if(typeof depth == "undefined") {
			depth = 100;
		}
		if(depth > 0) {
			depth--;
			var childNodes = element.childNodes;
			var noChildNodes = childNodes.length;
			for(var childNodeIndex = 0; childNodeIndex < noChildNodes; childNodeIndex++) {
				var childNode = childNodes[childNodeIndex];
				if(childNode.nodeType == 1) { // 1 == ELEMENT_NODE
					if(childNode.tagName == "INPUT" || childNode.tagName == "SELECT" || childNode.tagName == "TEXTAREA") {
						formControls.push(childNode);
					}
					else {
						formControls = formControls.concat(SitePoint.Html.Parsing.FormControls._FindChildren(childNode, depth));
					}
				}
			}
		}
		return formControls;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Parsing.FormControls._FindChildren");
		console.log(e);
		throw e;
	}
};


/**
 * @fileOverview
 * A collection of functions for parsing elements from HTML strings. Requires SitePoint.Type and SitePoint.Strings.Encoder.
 * @name SitePoint.Html.Parsing
 * @author Craig Anderson <craig@sitepoint.com>
 */

/**
 * The SitePoint namespace
 * @namespace
 * @ignore
 */
var SitePoint = SitePoint || {};

/**
 * The SitePoint.Html namespace
 * @namespace
 * @ignore
 */
SitePoint.Html = SitePoint.Html || {};

/**
 * The SitePoint.Html.Parsing namespace
 * @namespace
 */
SitePoint.Html.Parsing = SitePoint.Html.Parsing || {};

// Create Firebug stub if required
// Note that we should only use Firebug functions listed below.
window.console = window.console || { log: function() { /* do nothing */ } };


/**
 * Parse the given string of HTML for the given type of element.
 * @param {string} html The HTML to parse form controls from.
 * @param {string} name The name of elements to extract.
 * @param {boolean} [inOrder] Whether the elements are returned in the order they appear in the document or not.
 * @return {array} The elements found in the HTML. Note that this array is not orderered.
 * @author Craig Anderson <craig@sitepoint.com>
 * @static
 */
SitePoint.Html.Parsing.Parse = function(html, name, inOrder) {
	try {
		// set default values
		if(typeof inOrder == "undefined") {
			inOrder = false;
		}
		
		// check types
		if(typeof html != "string") {
			throw new TypeError("html must be a string");
		}
		if(typeof name != "string") {
			throw new TypeError("name must be a string");
		}
		if(typeof inOrder != "boolean") {
			throw new TypeError("inOrder must be a string");
		}
		
		var elements = [];
		// Create a temporary element. This will be used to instantiate our
		// elements by setting it's innerHTML.
		var tempElement = document.createElement("DIV");
		
		if(inOrder) {
			// instantiate the HTML
			tempElement.innerHTML = html;
			// iterate through eahc element in the HTML, recording any found INPUTs, SELECTs, and TEXTAREAs.
			elements = SitePoint.Html.Parsing._FindChildren(tempElement, name);
		}
		else {
			debugger;
			var encoder = SitePoint.Strings.Encoder.GetRegExpEncoder();
			
			// Find self-closing tags
			var closedRegExp = new RegExp("<" + encoder.encode(name) + "[^>]*\/>", "gi"); // <li[^>]*>/gi;
			while((match = closedRegExp.exec(html))) {
				tempElement.innerHTML = match[0];
				elements.push(tempElement.childNodes[0]);
			}
			
			// Find elements with start and end tags
			// Find where each element starts
			var startIndices = [];
			var startRegExp = new RegExp("<" + encoder.encode(name) + "[^\/>]*>", "gi"); // <li[^>]*>/gi;
			while((match = startRegExp.exec(html))) {
				startIndices.push(match.index);
			}
			// Find where each element ends
			var endIndices = [];
			var endRegExp = new RegExp("<\/" + encoder.encode(name) + ">", "gi"); // <\/li[^>]*>/gi;
			while((match = endRegExp.exec(html))) {
				endIndices.push(match.index + match[0].length);
			}
			// Make sure we've got the same number of starts and finishes.
			if(startIndices.length != endIndices.length) {
				throw new Error("Could not find a matching number of start and end tags.");
			}
			// Now create each element
			var noElements = startIndices.length;
			for(var i = 0; i < noElements; i++) {
				var startIndex = startIndices[i];
				var endIndex = endIndices[i];
				var elementHtml = html.slice(startIndex, endIndex);
				tempElement.innerHTML = elementHtml;
				elements.push(tempElement.childNodes[0].cloneNode(true));
			}
		}
		
		// Clean up
		tempElement = null;
		
		return elements;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Parsing.Parse");
		console.log(e);
		throw e;
	}
};

/**
 * Find child elements of a particular type.
 * @param {HTMLElement} element The HTML element to search for children of.
 * @param {string} name The name of elements to extract.
 * @param {integer} [depth] THe maximum depth to search to. If omitted, defaults to 100.
 * @return {array} Elements.
 * @author Craig Anderson <craig@sitepoint.com>
 * @private
 * @static
 */
SitePoint.Html.Parsing._FindChildren = function(element, name, depth) {
	try {
		var elements = [];
		if(typeof depth == "undefined") {
			depth = 100;
		}
		if(depth > 0) {
			depth--;
			name = name.toUpperCase();
			var childNodes = element.childNodes;
			var noChildNodes = childNodes.length;
			for(var childNodeIndex = 0; childNodeIndex < noChildNodes; childNodeIndex++) {
				var childNode = childNodes[childNodeIndex];
				if(childNode.nodeType == 1) { // 1 == ELEMENT_NODE
					if(childNode.tagName == name) {
						elements.push(childNode);
					}
					elements = elements.concat(SitePoint.Html.Parsing._FindChildren(childNode, name, depth));
				}
			}
		}
		return elements;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Html.Parsing._FindChildren");
		console.log(e);
		throw e;
	}
};


/**
 * The SitePoint namespace.
 * @namespace 
 */
var SitePoint = SitePoint || {};

// Create Firebug stub if required
// Note that we should only use Firebug functions listed below.
window.console = window.console || { log: function() { /* do nothing */ } };

/**
 * An image class that supports lazy loading images until they're needed
 * @constructor
 */
SitePoint.Image = function (src, width, height) {
	this._src = src;
	this._width = width;
	this._height = height
};

SitePoint.Image.prototype._src = null;
SitePoint.Image.prototype._width = null;
SitePoint.Image.prototype._height = null;
SitePoint.Image.prototype._ready = false;
SitePoint.Image.prototype._image = null;

/**
 * Indicates if an image is ready to be shown. Note that
 * this will implicitly trigger the image loading sequence
 */
SitePoint.Image.prototype.isReady = function () {
	if (this._image == null)
	{
		this.loadImage();
	}
	if (this._ready)
	{
		return true;
	}
	return false;
};

SitePoint.Image.prototype.loadImage = function () {
	this._image = new Image(this._width, this._height);
	thisImage = this;
	this._image.onload = function () {
		thisImage._ready = true;
	};
	this._image.src = this._src;
};

SitePoint.Image.prototype.getSrc = function () {
	return this._src;
};
/**
 * @fileOverview
 * A class which controls a function being called on a series of items. This
 * function is called once every second on each subsequent item.
 * Note that this class requires SitePoint.HashTable and SitePoint.Type.
 * @name SitePoint.LazyIterator
 * @author Craig Anderson <craig@sitepoint.com>
 */

/**
 * The SitePoint namespace.
 * @namespace
 * @ignore
 */
var SitePoint = SitePoint || {};

// Create Firebug stub if required
// Note that we should only use Firebug functions listed below.
window.console = window.console || { log: function() { /* do nothing */ } };

/**
 * Create a lazy iterator.
 * @example var li = new SitePoint.LazyIterator();
 * li.addItem("foo");
 * li.addItem("bar");
 * li.registerIterationFunction(function(x) {
 * 	print(x);
 * });
 * li.setIterationRate(1000); // set iteration rate to 1 second
 * li.start();
 * // ... wait one second ...
 * // prints "foo"
 * // ... wait one second ...
 * // prints "bar"
 * // ... wait one second ...
 * // prints "foo"
 * // ... wait one second ...
 * // prints "bar"
 * // ... and so on ...
 * // li2 is equivalent to li, and it will be automatically started.
 * var li2 = new SitePoint.LazyIterator(["foo", "bar"], function(x) {
 * 	print(x)
 * }, 1000);
 * @param {array} [items] The items to iterate over.
 * @param {function} [iterationFunction] The function to call on each item.
 * @param {integer} [iterationRate] How often to iterate in milliseconds. Defaults to 1000 milliseconds (one second).
 * @param {boolean} [autoStart] Whether to automatically start this lazy iterator. Defaults to true if both items and an iterationFunction are supplied in the constructor, false otherwise.
 * @author Craig Anderson <craig@sitepoint.com>
 * @constructor
 */
SitePoint.LazyIterator = function(items, iterationFunction, iterationRate, autoStart) {
	try {
		// Determine whther the iterator should be started immediately after
		// construction.
		if(typeof autoStart != "boolean") {
			autoStart = SitePoint.Type.IsArray(items) &&
					(typeof iterationFunction == "function");
		}
		// Set the items to iterate over
		if(!SitePoint.Type.IsArray(items)) {
			items = [];
		}
		this._items = items;
		// Set the first iteraction function
		this._iterationFunctions = [];
		if(typeof iterationFunction == "function") {
			this.registerIterationFunction(iterationFunction);
		}
		// Set iteration rate
		if(typeof iterationRate != "number") {
			iterationRate = 1000; // default to one second
		}
		this._iterationRate = iterationRate;
		// Initialse other internal variables
		this._itemPointer = 0;
		this._uniqueID = this._generateUniqueID();
		// Add this iterator to the global iterator registry.
		SitePoint.LazyIterator._Registry.add(this._uniqueID, this);
		// Auto-start if appropriate
		if(autoStart) {
			this.start();
		}
	}
	catch(e) {
		console.log("An unhandled error occurred in the SitePoint.LazyIterator constructor.");
		console.log(e);
		throw e;
	}
};

/**
 * @property {array} _items The items being iterated over.
 * @private
 */
SitePoint.LazyIterator.prototype._items = null;

/**
 * @property {integer} _itemPointer The index we're up to in the _items array.
 * @private
 */
SitePoint.LazyIterator.prototype._itemPointer = null;

/**
 * @property {integer} _intervalHandle The underlying interval handle for this iterator, as returned by setInterval.
 * @private
 */
SitePoint.LazyIterator.prototype._intervalHandle = null;

/**
 * @property {integer} _iterationRate How often the iterator should execute, in milliseconds.
 * @private
 */
SitePoint.LazyIterator.prototype._iterationRate = null;

/**
 * @property {array[function]} _iterationFunctions The functions to call on each item.
 * @private
 */
SitePoint.LazyIterator.prototype._iterationFunctions = null;

/**
 * @property {SitePoint.HashTable} _Registry All lazy iterators currently
 * running. Each lazy iterators make calls to setInterval which grabs the 
 * appropriate interator from this hash table, then calls tick() on the
 * iterator.
 * @private
 * @static
 */
SitePoint.LazyIterator._Registry = new SitePoint.HashTable();

/**
 * Add an item to iterate over.
 * @param {object} item The item to add.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.LazyIterator.prototype.addItem = function(item) {
	try {
		this._items.push(item);
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.LazyIterator::addItem");
		console.log(e);
		throw e;
	}
};

/**
 * Register a function to call on each item.
 * @example var globalInteger = 0;
 * var li = new SitePoint.LazyIterator();
 * li.addItem("foo");
 * li.addItem("bar");
 * // Register a few functions to call on each item.
 * li.registerIterationFunction(function(item) {
 * 	// print the item
 * 	print(item);
 * });
 * li.registerIterationFunction(function(item) {
 * 	// increment and display the global integer
 * 	globalInteger++;
 * 	print(globalInteger);
 * });
 * li.start();
 * // ... wait ...
 * // prints "foo", then "1"
 * // ... wait ...
 * // prints "bar", then "2"
 * // ... wait ...
 * // prints "foo", then "3"
 * // ... wait ...
 * // prints "bar", then "4"
 * // ... and so on ...
 * @param {function} f The function to register. This function should accept a single parameter, which is the item being iterated over.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.LazyIterator.prototype.registerIterationFunction = function(f) {
	try {
		this._iterationFunctions.push(f);
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.LazyIterator::registerIterationFunction");
		console.log(e);
		throw e;
	}
};

/**
 * Set the time between iterations. Note that this is the time between the
 * start of each iteration; that is, if your iteration functions take longer
 * than this interval, you will end up with multiple iterations running in
 * parallel.
 * Note that the new iteration rate takes effect immediately.
 * @param {integer} ms The iteration rate in milliseconds.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.LazyIterator.prototype.setIterationRate = function(ms) {
	try {
		this._iterationRate = ms;
		// Restart so new iteration rate takes effect.
		if(this.isRunning()) {
			this.stop();
			this.start();
		}
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.LazyIterator::setIterationRate");
		console.log(e);
		throw e;
	}
};

/**
 * Start the lazy iterator.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.LazyIterator.prototype.start = function() {
	try {
		var intervalString = "SitePoint.LazyIterator._Registry.get(\"" + this._uniqueID + "\").tick();";
		this._intervalHandle = setInterval(intervalString, this._iterationRate);
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.LazyIterator::start");
		console.log(e);
		throw e;
	}
};

/**
 * Stop the lazy iterator.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.LazyIterator.prototype.stop = function() {
	try {
		clearInterval(this._intervalHandle);
		this._intervalHandle = null;
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.LazyIterator::stop");
		console.log(e);
		throw e;
	}
};

/**
 * Invoke an iteration, as if the iterationInterval has passed.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.LazyIterator.prototype.tick = function() {
	try {
		if(this._items.length !== 0) {
			// Stop item pointer from overflowing the number of items
			if(this._itemPointer >= this._items.length) {
				this._itemPointer = 0;
			}
			// Get the item pointed at by the item pointer
			var item = this._items[this._itemPointer];
			// Increment the item pointer for next time.
			this._itemPointer++;
			// Call functions on the current item
			this._callIterationFunctions(item);
		}
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.LazyIterator::tick");
		console.log(e);
		throw e;
	}
};

/**
 * Determines if this lazy iterator is currently running.
 * @return {boolean} Whether the iterator is running or not.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.LazyIterator.prototype.isRunning = function() {
	try {
		return (typeof this._intervalHandle == "number");
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.LazyIterator::isRunning");
		console.log(e);
		throw e;
	}
};

/**
 * Call the registered iteration functions on the given item.
 * @param {object} item The item being iterated over.
 * @author Craig Anderson <craig@sitepoint.com>
 * @private
 */
SitePoint.LazyIterator.prototype._callIterationFunctions = function(item) {
	try {
		var noIterationFunctions = this._iterationFunctions.length;
		for(var i = 0; i < noIterationFunctions; i++) {
			var iterationFunction = this._iterationFunctions[i];
			try {
				iterationFunction(item);
			}
			catch(e) {
				console.log("An unhandled error occurred in one of the iteration functions");
				console.log(e);
				throw e;
			}
		}
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.LazyIterator::_callIterationFunctions");
		console.log(e);
		throw e;
	}
};

/**
 * Generate a unique ID for this lazy iterator.
 * @return {string} The unique ID for this lazy iterator.
 * @author Craig Anderson <craig@sitepoint.com>
 * @private
 */
SitePoint.LazyIterator.prototype._generateUniqueID = function() {
	try {
		// Generate a unique ID
		var uid = (new Date()).toString() + Math.random();
		// Ensure nothing else with that ID exists in the LazyIterator registry.
		if(SitePoint.LazyIterator._Registry.get(uid)) {
			throw new Error("The ID generated was not unique.");
		}
		// Return the ID
		return uid;
	}
	catch(e) {
		console.log("An unexpected error occurred in SitePoint.LazyIterator::_generateUniqueID");
		console.log(e);
		throw e;
	}
};
/**
 * @fileOverview
 * Automatically display the results of a SitePoint.Profiling.Reporter after a
 * given period of inactivity, or after an overriding timeout expires.
 * @name SitePoint.Profiling.AutoReport
 * @author Craig Anderson <craig@sitepoint.com>
 */

/**
 * The SitePoint namespace.
 * @namespace
 * @ignore
 */
var SitePoint = SitePoint || {};

/**
 * The SitePoint.Profiling namespace.
 * @namespace
 */
SitePoint.Profiling = SitePoint.Profiling || {};

// Create Firebug stub if required
// Note that we should only use Firebug functions listed below.
window.console = window.console || { log: function() { /* do nothing */ } };

/**
 * Automatically displays a report after a given period of inactivity, or after an overriding timeout expires.
 * This method should not be called directly; use SitePoint.Profiling.AutoReport.GetInstance instead.
 * @example // Set up some timers, a reporter, and an autoreport
 * var t1 = new SitePoint.Profiling.Timer("My first timer");
 * var t2 = new SitePoint.Profiling.Timer("My second timer");
 * var reporter = new SitePoint.Profiling.Reporter([t1, t2]);
 * var autoReport = SitePoint.Profiling.AutoReport.GetInstance(true, reporter);
 *
 * // We can either wait for the inactivity timeout...
 * t1.start();
 * // ... some stuff to time ...
 * t1.stop(); // The report will be displayed in ten seconds.
 *
 * // Or we can wait for the overriding timeout...
 * t1.start();
 * // The report will be displayed once this script has been running for 30 seconds.
 * while(true) {
 * 	t2.start();
 * 	// ... some stuff to time ...
 * 	t2.stop();
 * }
 * t1.stop();
 * @param {SitePoint.Profiling.Reporter} reporter The reporter to be displayed.
 * @param {integer} inactivityTimeout The inactivity timeout in milliseconds. Set to 10 seconds by default.
 * @param {integer} timeout The overall timeout in milliseconds. Set to 30 seconds by default.
 * @author Craig Anderson <craig@sitepoint.com>
 * @constructor
 */
SitePoint.Profiling.AutoReport = function(reporter, inactivityTimeout, timeout) {
	try {
		if(SitePoint.Profiling.AutoReport._NoInstances > 0) {
			throw new Error("An AutoReport has already been instantiated.\n\n" +
					"Please make use of that instance of the class, or access it via " +
					"the SitePoint.Profiling.AutoReport.GetInstance method.");
		}
		
		// Check the report paramter was supplied and that it's of the expected
		// type.
		if(typeof reporter != "object" || typeof reporter.report != "function") {
			throw new TypeError();
		}
		
		// Initialise reporter
		this._wrapReporterFunctions(reporter);
		this._reporter = reporter;
		
		// Initialise timeouts
		if(typeof inactivityTimeout != "number") {
			inactivityTimeout = 10000; // ten seconds
		}
		this._inactivityTimeout = inactivityTimeout; // ten seconds
		if(typeof timeout != "number") {
			timeout = 30000; // thirty seconds
		}
		this._timeout = timeout;
		
		// Create default report display function
		this._showReport = function(reporter) {
			var reportString = reporter.report();
			reportString += "\n\nTo customise how this message is displayed, call the setShowReportFunction method of the SitePoint.Profiling.AutoReport class.";
			alert(reportString);
		};
		
		// Set up overall timeout
		this._timeoutHandle = setTimeout(function() {
			var autoReport = SitePoint.Profiling.AutoReport.GetInstance(false);
			if(autoReport) {
				autoReport.handleTimeout();
			}
		}, this._timeout);
		
		// Call handle activity to start inactivity timeout
		this.handleActivity();
		
		// Signify that this instance has been created and store this instance
		SitePoint.Profiling.AutoReport._NoInstances = 1;
		SitePoint.Profiling.AutoReport._Instance = this;
	}
	catch(e) {
		console.log("An unhandled error occurred in the SitePoint.Profiling.AutoReport constructor.");
		console.log(e);
		throw e;
	}
};

/**
 * @property {SitePoint.Profiling.Reporter} _reporter The reporter to call once the timeout has expired.
 * @private
 */
SitePoint.Profiling.AutoReport.prototype._reporter = null;

/**
 * @property {integer} _inactivityTimeout The number of milliseconds to wait after a method call until the report is displayed.
 * @private
 */
SitePoint.Profiling.AutoReport.prototype._inactivityTimeout = null;

/**
 * @property {integer} _inactivityTimeoutHandle The timeout handle for the inactivity timeout.
 * @private
 */
SitePoint.Profiling.AutoReport.prototype._inactivityTimeoutHandle = null;

/**
 * @property {function} _showReport The function that's called when one of the timeouts expire. This function should accept one paramter: a SitePoint.Profiling.Reporter.
 * @private
 */
SitePoint.Profiling.AutoReport.prototype._showReport = null;

/**
 * @property {integer} _timeout The number of milliseconds to wait to display the report, regardless of any ongoing activity.
 * @private
 */
SitePoint.Profiling.AutoReport.prototype._timeout = null;

/**
 * @property {integer} _timeoutHandle The timeout handle for the overriding timeout.
 * @private
 */
SitePoint.Profiling.AutoReport.prototype._timeoutHandle = null;


/**
 * Get the reporter registered with this auto report.
 * @return {SitePoint.Profiling.Reporter} The registered reporter.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Profiling.AutoReport.prototype.getReporter = function() {
	try {
		return this._reporter;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Profiling.AutoReport::setReport");
		console.log(e);
		throw e;
	}
};

/**
 * Set the function which will show display the report. This function should
 * accept one parameter, a SitePoint.Profiling.Reporter. The report string can
 * be accessed by calling report on this object.
 * @example autoReport.setShowReportFunction(function(reporter) {
 * 	alert(reporter.report());
 * });
 * @param {function} f The function.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Profiling.AutoReport.prototype.setShowReportFunction = function(f) {
	try {
		this._showReport = f;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Profiling.AutoReport::showReport");
		console.log(e);
		throw e;
	}
};

/**
 * Handle any activity on any timer or reporter. This method resets the inactivity timeout. This method should not be called directly.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Profiling.AutoReport.prototype.handleActivity = function() {
	try {
		clearTimeout(this._inactivityTimeoutHandle);
		this._inactivityTimeoutHandle = setTimeout(function() {
			var autoReport = SitePoint.Profiling.AutoReport.GetInstance(false);
			if(autoReport) {
				autoReport.handleTimeout();
			}
		}, this._inactivityTimeout);
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Profiling.AutoReport::handleActivity");
		console.log(e);
		throw e;
	}
};


/**
 * Wrap the start and stop methods of a timer in calls to the AutoReporter's handleActivity method. This method is public for testing purposes and should not be called directly.
 * @param {SitePoint.Profiling.Timer} timer The timer to wrap the methods of.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Profiling.AutoReport.prototype.wrapTimerFunctions = function(timer) {
	try {
		var callHandleActivity = function() {
			var autoReport = SitePoint.Profiling.AutoReport.GetInstance(false);
			// If there is no AutoReport instance, make this an empty function.
			if(!autoReport) {
				arguments.callee = function() {};
			}
			else {
				// Otherwise, call handleActivity.
				autoReport.handleActivity();
			}
		};
		SitePoint.Wrapping.WrapMethod(timer, "start", callHandleActivity);
		SitePoint.Wrapping.WrapMethod(timer, "stop", callHandleActivity);
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Profiling.AutoReport::wrapTimerFunctions");
		console.log(e);
		throw e;
	}
};

/**
 * Called when either the interval timeout or the overall timeout expires.
 * Stops all timeouts, and displays the report by calling the function registered with setShowReportFunction.
 * This method is public for testing purposes and should not be called directly.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Profiling.AutoReport.prototype.handleTimeout = function() {
	try {
		// Clear the timeouts
		clearTimeout(this._inactivityTimeoutHandle);
		clearTimeout(this._timeoutHandle);
		
		// Call the show report function
		try {
			this._showReport(this._reporter);
		}
		catch(e) {
			console.log("An unhandled error occurred in the registered show report function");
			console.log(e);
			throw e;
		}
		
		// Destroy this class
		this.unload();
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Profiling.AutoReport::handleTimeout");
		console.log(e);
		throw e;
	}
};

/**
 * Destroy this class.
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Profiling.AutoReport.prototype.unload = function() {
	
	// Clear timeouts
	clearTimeout(this._inactivityTimeoutHandle);
	clearTimeout(this._timeoutHandle);
	
	// Call any unload methods of members
	if(this._reporter !== null) {
		if(typeof this._reporter.unload == "function") {
			try {
				this._reporter.unload();
			}
			catch(e) {
				console.log("An unhandled error occurred in the reporter's unload method.");
				console.log(e);
				console.log("Supressing this error.");
			}
		}
	}
	
	
	// Set members to null
	this._showReport = null;
	this._reporter = null;
	
	// Reset static members to allow subsequent creations of this class.
	SitePoint.Profiling.AutoReport._NoInstances = 0;
	SitePoint.Profiling.AutoReport._Instance = null;
};

/**
 * Make sure all timers currently registered with the reporter and any timers subsequently added to the report notify this auto report of activity whenever their stop or start methods are called.
 * @example var t1 = new SitePoint.Profiling.Timer("My first timer");
 * var t2 = new SitePoint.Profiling.Timer("My second timer");
 * var t3 = new SitePoint.Profiling.Timer("My third timer");
 * var reporter = new SitePoint.Profiling.Reporter([t1, t2]);
 * var autoReport = SitePoint.Profiling.AutoReport.GetInstance(true, reporter);
 * // As the AutoReport constructor calls _wrapReporterFunctions, calls to
 * // t1.start(), t1.stop(), t2.start() and t2.stop() will now automatically
 * // call autoReport.handleActivity().
 * reporter.addTimer(t3);
 * // Calls to t3.start() and t3.stop() will now call autoReport.handleActivity()
 * // too.
 * @param {SitePoint.Profiling.Reporter} reporter The reporter to wrap.
 * @author Craig Anderson <craig@sitepoint.com>
 * @private
 */
SitePoint.Profiling.AutoReport.prototype._wrapReporterFunctions = function(reporter) {
	try {
		// Modify the timers already added to the reporter
		var timers = reporter.getTimers();
		var noTimers = timers.length;
		for(var i = 0; i < noTimers; i++) {
			var timer = timers[i];
			this.wrapTimerFunctions(timer);
		}

		// Wrap the add method of the reporter so any subsequent timers 
		// added to the report get the same treatment.
		var callWrapTimerFunctions = function(timer) {
			// If there is no AutoReport instance, make this an empty function.
			if(SitePoint.Profiling.AutoReport._NoInstances != 1) {
				arguments.callee = function() {};
			}
			else {
				// Otherwise, check if timer is an array
				if(!SitePoint.Type.IsArray(timer)) {
					// Timer isn't an array, so let's modify it.
					var autoReport = SitePoint.Profiling.AutoReport.GetInstance(false);
					if(autoReport) {
						autoReport.wrapTimerFunctions(timer);
					}
				}
			}
		};
		SitePoint.Wrapping.WrapMethod(reporter, "add", callWrapTimerFunctions);
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Profiling.AutoReport::_wrapReporterFunctions");
		console.log(e);
		throw e;
	}
};

SitePoint.Profiling.AutoReport._NoInstances = 0;
SitePoint.Profiling.AutoReport._Instance = null;

/**
 * Create an auto report, or get the existing auto report.
 * @param {boolean} allowCreation Whether to create an instance if one doesn't already exist. If set to false and an instance isn't already created, null is returned.
 * @param {SitePoint.Profiling.Reporter} [reporter] The reporter to be displayed. If an instance doesn't exist and this is undefined, an error is returned. If this is defined and an instance already exists, this is ignored.
 * @param {integer} [inactivityTimeout] The inactivity timeout in milliseconds. Set to 10 seconds by default. If this is defined and an instance already exists, this is ignored.
 * @param {integer} [timeout] The overall timeout in milliseconds. Set to 30 seconds by default. If this is defined and an instance already exists, this is ignored.
 * @return {SitePoint.Profiling.AutoReport} The auto report.
 * @author Craig Anderson <craig@sitepoint.com>
 * @static
 */
SitePoint.Profiling.AutoReport.GetInstance = function(allowCreation, reporter, inactivityTimeout, timeout) {
	if(typeof allowCreation == "undefined") {
		allowCreation = true;
	}
	if(SitePoint.Profiling.AutoReport._NoInstances > 0) {
		return SitePoint.Profiling.AutoReport._Instance;
	}
	else {
		if(allowCreation) {
			SitePoint.Profiling.AutoReport._Instance = new SitePoint.Profiling.AutoReport(reporter, inactivityTimeout, timeout);
			return SitePoint.Profiling.AutoReport._Instance;
		}
		else {
			return null;
		}
	}
};
/**
 * @fileOverview
 * A class which automatically attaches SitePoint.Profiling.Timers to each
 * method of a class.
 * @name SitePoint.Profiling.ObjectWrapper
 * @author Craig Anderson <craig@sitepoint.com>
 */

/**
 * The SitePoint namespace.
 * @namespace
 * @ignore
 */
var SitePoint = SitePoint || {};

/**
 * The SitePoint.Profiling namespace.
 * @namespace
 * @ignore
 */
SitePoint.Profiling = SitePoint.Profiling || {};

// Create Firebug stub if required
// Note that we should only use Firebug functions listed below.
window.console = window.console || { log: function() { /* do nothing */ } };

/**
 * An object to attach SitePoint.Profiling.Timers to each method of an object.
 * @example // Create a hash table to profile
 * var ht = new SitePoint.HashTable();
 * // Wrap each method of the hash table object in calls to timers
 * var objWrapper = new SitePoint.Profiling.ObjectWrapper(ht, "My Hash Table");
 * // Create a reporter
 * var reporter = new SitePoint.Profiling.Reporter(objWrapper.getTimers());
 * // Do some stuff with the hash table
 * for(var i = 0; i < 100; i++) {
 * 	ht.add(i, i);
 * }
 * var sum = 0;
 * for(var i = 0; i < 100; i++) {
 * 	sum = sum + ht.get(i);
 * }
 * // Get the report
 * var report = reporter.report();
 * // report contains the following:
 * // Test Hash Table _hashFunction    7 milliseconds
 * // Test Hash Table get              28 milliseconds
 * // Test Hash Table add              54 milliseconds
 * // Test Hash Table remove           25 milliseconds
 * // Test Hash Table setHashFunction  0 milliseconds
 * // Test Hash Table callHashFunction 25 milliseconds
 * // Test Hash Table toArray          0 milliseconds
 * // Test Hash Table getKeys          0 milliseconds
 * // Test Hash Table each             0 milliseconds
 * // Test Hash Table unload           0 milliseconds
 * // Test Hash Table _getBucket       45 milliseconds
 * 
 * @param {object} obj The object to wrap.
 * @param {string} [prefix] The prefix to include with each timer. If omitted, obj.toString() will be used.
 * @author Craig Anderson <craig@sitepoint.com>
 * @constructor
 */
SitePoint.Profiling.ObjectWrapper = function(obj, prefix) {
	try {
		// Initialise prefix
		if(typeof prefix == "undefined") {
			prefix = obj.toString();
		}
		
		// Initialise the object wrapper registry
		if(SitePoint.Profiling.ObjectWrapper._Registry === null) {
			SitePoint.Profiling.ObjectWrapper._Registry = new SitePoint.HashTable();
		}

		// Get a unique ID for this wrapper
		this._uniqueID = this._generateUniqueID();
		SitePoint.Profiling.ObjectWrapper._Registry.add(this._uniqueID, this);

		// Initialise the array of timers
		this._timers = new SitePoint.HashTable();

		// Build a list of methods of this object
		var methodNames = [];
		for(var propertyName in obj) {
			var property = obj[propertyName];
			if(typeof property == "function") {
				methodNames.push(propertyName);
			}
		}

		// Iterate through each method of this object
		var noMethodNames = methodNames.length;
		for(var i = 0; i < noMethodNames; i++) {
			var methodName = methodNames[i];

			// Create a timer for this method
			var timer = new SitePoint.Profiling.Timer(prefix + " " + methodName);
			this._timers.add(methodName, timer);


			// Define the functions as strings that we'll call with eval.
			// We need to do this so we can hard-code the unique ID and method
			// names into these functions.
			var beforeFunctionString = "beforeFunction = function() {\n" +
			"	var objectWrapper = SitePoint.Profiling.ObjectWrapper._Registry.get(\"" + this._uniqueID + "\");\n" +
			"	var timer = objectWrapper._timers.get(\"" + methodName + "\");\n" +
			"	if(timer) timer.start();\n" +
			"};";

			var afterFunctionString = "afterFunction = function() {\n" +
			"	var objectWrapper = SitePoint.Profiling.ObjectWrapper._Registry.get(\"" + this._uniqueID + "\");\n" +
			"	var timer = objectWrapper._timers.get(\"" + methodName + "\");\n" +
			"	if(timer) timer.stop();\n" +
			"};";
			
			// Create default functions in case something goes wrong while eval-ing
			// the above strings.
			var beforeFunction = function() {
				throw new Error("There was an error creating the before function");
			};
			var afterFunciton = function() {
				throw new Error("There was an error creating the after function");
			};
			
			// Now evaluate the above strings which should re-define beforeFunction and afterFunction
			eval(beforeFunctionString);
			eval(afterFunctionString);
			
			// Wrap the method in these functions
			SitePoint.Wrapping.WrapMethod(obj, methodName, beforeFunction, afterFunction);
		}
	}
	catch(e) {
		console.log("An unhandled error occurred in the SitePoint.Profiling.ObjectWrapper constructor");
		console.log(e);
		throw e;
	}
};

SitePoint.Profiling.ObjectWrapper.prototype._uniqueID = null;
SitePoint.Profiling.ObjectWrapper.prototype._timers = null;

/**
 * Get an array of timers managed by this object wrapper.
 * @return {array} An array of SitePoint.Profiling.Timers
 * @author Craig Anderson <craig@sitepoint.com>
 */
SitePoint.Profiling.ObjectWrapper.prototype.getTimers = function() {
	try {
		return this._timers.toArray();
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Profiling.ObjectWrapper::getTimers");
		console.log(e);
		throw e;
	}
};

/**
 * Generate a unique ID for this object wrapper.
 * @return {string} A unique ID string.
 * @author Craig Anderson <craig@sitepoint.com>
 * @private
 */
SitePoint.Profiling.ObjectWrapper.prototype._generateUniqueID = function() {
	try {
		var uniqueID = new String((new Date()).getTime()) + new String(Math.random());
		if(SitePoint.Profiling.ObjectWrapper._Registry.get(uniqueID)) {
			throw new Error("The generated ID was not unique.");
		}
		return uniqueID;
	}
	catch(e) {
		console.log("An unhandled error occurred in SitePoint.Profiling.ObjectWrapper::_generateUniqueID");
		console.log(e);
		throw e;
	}
};

/**
 * @property {SitePoint.HashTable} All created object wrappers.
 * @author Craig Anderson <craig@sitepoint.com>
 * @private
 * @static
 */
SitePoint.Profiling.ObjectWrapper._Registry