text: /**
   * Copyright 2014 Tim Down.
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  /**
   * log4javascript
   *
   * log4javascript is a logging framework for JavaScript based on log4j
   * for Java. This file contains all core log4javascript code and is the only
   * file required to use log4javascript, unless you require support for
   * document.domain, in which case you will also need console.html, which must be
   * stored in the same directory as the main log4javascript.js file.
   *
   * Author: Tim Down <tim@log4javascript.org>
   * Version: 1.4.11
   * Edition: log4javascript
   * Build date: 19 February 2015
   * Website: http://log4javascript.org
   */
  (function(factory, root) {
  	if (typeof define == "function") {
  		// AMD. Register as an anonymous module.
  		define(factory);
  	} else if (typeof module != "undefined" && typeof exports == "object") {
  		// Node/CommonJS style
  		module.exports = factory();
  	} else {
  		// No AMD or CommonJS support so we place log4javascript in (probably) the global variable
  		root.log4javascript = factory();
  	}
  })(function() {
  	// Array-related stuff. Next three methods are solely for IE5, which is missing them
  	if (!Array.prototype.push) {
  		Array.prototype.push = function() {
  			for (var i = 0, len = arguments.length; i < len; i++){
  				this[this.length] = arguments[i];
  			}
  			return this.length;
  		};
  	}
  
  	if (!Array.prototype.shift) {
  		Array.prototype.shift = function() {
  			if (this.length > 0) {
  				var firstItem = this[0];
  				for (var i = 0, len = this.length - 1; i < len; i++) {
  					this[i] = this[i + 1];
  				}
  				this.length = this.length - 1;
  				return firstItem;
  			}
  		};
  	}
  
  	if (!Array.prototype.splice) {
  		Array.prototype.splice = function(startIndex, deleteCount) {
  			var itemsAfterDeleted = this.slice(startIndex + deleteCount);
  			var itemsDeleted = this.slice(startIndex, startIndex + deleteCount);
  			this.length = startIndex;
  			// Copy the arguments into a proper Array object
  			var argumentsArray = [];
  			for (var i = 0, len = arguments.length; i < len; i++) {
  				argumentsArray[i] = arguments[i];
  			}
  			var itemsToAppend = (argumentsArray.length > 2) ?
  				itemsAfterDeleted = argumentsArray.slice(2).concat(itemsAfterDeleted) : itemsAfterDeleted;
  			for (i = 0, len = itemsToAppend.length; i < len; i++) {
  				this.push(itemsToAppend[i]);
  			}
  			return itemsDeleted;
  		};
  	}
  
  	/* ---------------------------------------------------------------------- */
  
  	function isUndefined(obj) {
  		return typeof obj == "undefined";
  	}
  
  	/* ---------------------------------------------------------------------- */
  	// Custom event support
  
  	function EventSupport() {}
  
  	EventSupport.prototype = {
  		eventTypes: [],
  		eventListeners: {},
  		setEventTypes: function(eventTypesParam) {
  			if (eventTypesParam instanceof Array) {
  				this.eventTypes = eventTypesParam;
  				this.eventListeners = {};
  				for (var i = 0, len = this.eventTypes.length; i < len; i++) {
  					this.eventListeners[this.eventTypes[i]] = [];
  				}
  			} else {
  				handleError("log4javascript.EventSupport [" + this + "]: setEventTypes: eventTypes parameter must be an Array");
  			}
  		},
  
  		addEventListener: function(eventType, listener) {
  			if (typeof listener == "function") {
  				if (!array_contains(this.eventTypes, eventType)) {
  					handleError("log4javascript.EventSupport [" + this + "]: addEventListener: no event called '" + eventType + "'");
  				}
  				this.eventListeners[eventType].push(listener);
  			} else {
  				handleError("log4javascript.EventSupport [" + this + "]: addEventListener: listener must be a function");
  			}
  		},
  
  		removeEventListener: function(eventType, listener) {
  			if (typeof listener == "function") {
  				if (!array_contains(this.eventTypes, eventType)) {
  					handleError("log4javascript.EventSupport [" + this + "]: removeEventListener: no event called '" + eventType + "'");
  				}
  				array_remove(this.eventListeners[eventType], listener);
  			} else {
  				handleError("log4javascript.EventSupport [" + this + "]: removeEventListener: listener must be a function");
  			}
  		},
  
  		dispatchEvent: function(eventType, eventArgs) {
  			if (array_contains(this.eventTypes, eventType)) {
  				var listeners = this.eventListeners[eventType];
  				for (var i = 0, len = listeners.length; i < len; i++) {
  					listeners[i](this, eventType, eventArgs);
  				}
  			} else {
  				handleError("log4javascript.EventSupport [" + this + "]: dispatchEvent: no event called '" + eventType + "'");
  			}
  		}
  	};
  
  	/* -------------------------------------------------------------------------- */
  
  	var applicationStartDate = new Date();
  	var uniqueId = "log4javascript_" + applicationStartDate.getTime() + "_" +
  		Math.floor(Math.random() * 100000000);
  	var emptyFunction = function() {};
  	var newLine = "\r\n";
  	var pageLoaded = false;
  
  	// Create main log4javascript object; this will be assigned public properties
  	function Log4JavaScript() {}
  	Log4JavaScript.prototype = new EventSupport();
  
  	var log4javascript = new Log4JavaScript();
  	log4javascript.version = "1.4.11";
  	log4javascript.edition = "log4javascript";
  
  	/* -------------------------------------------------------------------------- */
  	// Utility functions
  
  	function toStr(obj) {
  		if (obj && obj.toString) {
  			return obj.toString();
  		} else {
  			return String(obj);
  		}
  	}
  
  	function getExceptionMessage(ex) {
  		if (ex.message) {
  			return ex.message;
  		} else if (ex.description) {
  			return ex.description;
  		} else {
  			return toStr(ex);
  		}
  	}
  
  	// Gets the portion of the URL after the last slash
  	function getUrlFileName(url) {
  		var lastSlashIndex = Math.max(url.lastIndexOf("/"), url.lastIndexOf("\\"));
  		return url.substr(lastSlashIndex + 1);
  	}
  
  	// Returns a nicely formatted representation of an error
  	function getExceptionStringRep(ex) {
  		if (ex) {
  			var exStr = "Exception: " + getExceptionMessage(ex);
  			try {
  				if (ex.lineNumber) {
  					exStr += " on line number " + ex.lineNumber;
  				}
  				if (ex.fileName) {
  					exStr += " in file " + getUrlFileName(ex.fileName);
  				}
  			} catch (localEx) {
  				logLog.warn("Unable to obtain file and line information for error");
  			}
  			if (showStackTraces && ex.stack) {
  				exStr += newLine + "Stack trace:" + newLine + ex.stack;
  			}
  			return exStr;
  		}
  		return null;
  	}
  
  	function bool(obj) {
  		return Boolean(obj);
  	}
  
  	function trim(str) {
  		return str.replace(/^\s+/, "").replace(/\s+$/, "");
  	}
  
  	function splitIntoLines(text) {
  		// Ensure all line breaks are \n only
  		var text2 = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
  		return text2.split("\n");
  	}
  
  	var urlEncode = (typeof window.encodeURIComponent != "undefined") ?
  		function(str) {
  			return encodeURIComponent(str);
  		}: 
  		function(str) {
  			return escape(str).replace(/\+/g, "%2B").replace(/"/g, "%22").replace(/'/g, "%27").replace(/\//g, "%2F").replace(/=/g, "%3D");
  		};
  
  	function array_remove(arr, val) {
  		var index = -1;
  		for (var i = 0, len = arr.length; i < len; i++) {
  			if (arr[i] === val) {
  				index = i;
  				break;
  			}
  		}
  		if (index >= 0) {
  			arr.splice(index, 1);
  			return true;
  		} else {
  			return false;
  		}
  	}
  
  	function array_contains(arr, val) {
  		for(var i = 0, len = arr.length; i < len; i++) {
  			if (arr[i] == val) {
  				return true;
  			}
  		}
  		return false;
  	}
  
  	function extractBooleanFromParam(param, defaultValue) {
  		if (isUndefined(param)) {
  			return defaultValue;
  		} else {
  			return bool(param);
  		}
  	}
  
  	function extractStringFromParam(param, defaultValue) {
  		if (isUndefined(param)) {
  			return defaultValue;
  		} else {
  			return String(param);
  		}
  	}
  
  	function extractIntFromParam(param, defaultValue) {
  		if (isUndefined(param)) {
  			return defaultValue;
  		} else {
  			try {
  				var value = parseInt(param, 10);
  				return isNaN(value) ? defaultValue : value;
  			} catch (ex) {
  				logLog.warn("Invalid int param " + param, ex);
  				return defaultValue;
  			}
  		}
  	}
  
  	function extractFunctionFromParam(param, defaultValue) {
  		if (typeof param == "function") {
  			return param;
  		} else {
  			return defaultValue;
  		}
  	}
  
  	function isError(err) {
  		return (err instanceof Error);
  	}
  
  	if (!Function.prototype.apply){
  		Function.prototype.apply = function(obj, args) {
  			var methodName = "__apply__";
  			if (typeof obj[methodName] != "undefined") {
  				methodName += String(Math.random()).substr(2);
  			}
  			obj[methodName] = this;
  
  			var argsStrings = [];
  			for (var i = 0, len = args.length; i < len; i++) {
  				argsStrings[i] = "args[" + i + "]";
  			}
  			var script = "obj." + methodName + "(" + argsStrings.join(",") + ")";
  			var returnValue = eval(script);
  			delete obj[methodName];
  			return returnValue;
  		};
  	}
  
  	if (!Function.prototype.call){
  		Function.prototype.call = function(obj) {
  			var args = [];
  			for (var i = 1, len = arguments.length; i < len; i++) {
  				args[i - 1] = arguments[i];
  			}
  			return this.apply(obj, args);
  		};
  	}
  
  	/* ---------------------------------------------------------------------- */
  	// Simple logging for log4javascript itself
  
  	var logLog = {
  		quietMode: false,
  
  		debugMessages: [],
  
  		setQuietMode: function(quietMode) {
  			this.quietMode = bool(quietMode);
  		},
  
  		numberOfErrors: 0,
  
  		alertAllErrors: false,
  
  		setAlertAllErrors: function(alertAllErrors) {
  			this.alertAllErrors = alertAllErrors;
  		},
  
  		debug: function(message) {
  			this.debugMessages.push(message);
  		},
  
  		displayDebug: function() {
  			alert(this.debugMessages.join(newLine));
  		},
  
  		warn: function(message, exception) {
  		},
  
  		error: function(message, exception) {
  			if (++this.numberOfErrors == 1 || this.alertAllErrors) {
  				if (!this.quietMode) {
  					var alertMessage = "log4javascript error: " + message;
  					if (exception) {
  						alertMessage += newLine + newLine + "Original error: " + getExceptionStringRep(exception);
  					}
  					alert(alertMessage);
  				}
  			}
  		}
  	};
  	log4javascript.logLog = logLog;
  
  	log4javascript.setEventTypes(["load", "error"]);
  
  	function handleError(message, exception) {
  		logLog.error(message, exception);
  		log4javascript.dispatchEvent("error", { "message": message, "exception": exception });
  	}
  
  	log4javascript.handleError = handleError;
  
  	/* ---------------------------------------------------------------------- */
  
  	var enabled = !((typeof log4javascript_disabled != "undefined") &&
  					log4javascript_disabled);
  
  	log4javascript.setEnabled = function(enable) {
  		enabled = bool(enable);
  	};
  
  	log4javascript.isEnabled = function() {
  		return enabled;
  	};
  
  	var useTimeStampsInMilliseconds = true;
  
  	log4javascript.setTimeStampsInMilliseconds = function(timeStampsInMilliseconds) {
  		useTimeStampsInMilliseconds = bool(timeStampsInMilliseconds);
  	};
  
  	log4javascript.isTimeStampsInMilliseconds = function() {
  		return useTimeStampsInMilliseconds;
  	};
  	
  
  	// This evaluates the given expression in the current scope, thus allowing
  	// scripts to access private variables. Particularly useful for testing
  	log4javascript.evalInScope = function(expr) {
  		return eval(expr);
  	};
  
  	var showStackTraces = false;
  
  	log4javascript.setShowStackTraces = function(show) {
  		showStackTraces = bool(show);
  	};
  
  	/* ---------------------------------------------------------------------- */
  	// Levels
  
  	var Level = function(level, name) {
  		this.level = level;
  		this.name = name;
  	};
  
  	Level.prototype = {
  		toString: function() {
  			return this.name;
  		},
  		equals: function(level) {
  			return this.level == level.level;
  		},
  		isGreaterOrEqual: function(level) {
  			return this.level >= level.level;
  		}
  	};
  
  	Level.ALL = new Level(Number.MIN_VALUE, "ALL");
  	Level.TRACE = new Level(10000, "TRACE");
  	Level.DEBUG = new Level(20000, "DEBUG");
  	Level.INFO = new Level(30000, "INFO");
  	Level.WARN = new Level(40000, "WARN");
  	Level.ERROR = new Level(50000, "ERROR");
  	Level.FATAL = new Level(60000, "FATAL");
  	Level.OFF = new Level(Number.MAX_VALUE, "OFF");
  
  	log4javascript.Level = Level;
  
  	/* ---------------------------------------------------------------------- */
  	// Timers
  
  	function Timer(name, level) {
  		this.name = name;
  		this.level = isUndefined(level) ? Level.INFO : level;
  		this.start = new Date();
  	}
  
  	Timer.prototype.getElapsedTime = function() {
  		return new Date().getTime() - this.start.getTime();
  	};
  
  	/* ---------------------------------------------------------------------- */
  	// Loggers
  
  	var anonymousLoggerName = "[anonymous]";
  	var defaultLoggerName = "[default]";
  	var nullLoggerName = "[null]";
  	var rootLoggerName = "root";
  
  	function Logger(name) {
  		this.name = name;
  		this.parent = null;
  		this.children = [];
  
  		var appenders = [];
  		var loggerLevel = null;
  		var isRoot = (this.name === rootLoggerName);
  		var isNull = (this.name === nullLoggerName);
  
  		var appenderCache = null;
  		var appenderCacheInvalidated = false;
  		
  		this.addChild = function(childLogger) {
  			this.children.push(childLogger);
  			childLogger.parent = this;
  			childLogger.invalidateAppenderCache();
  		};
  
  		// Additivity
  		var additive = true;
  		this.getAdditivity = function() {
  			return additive;
  		};
  
  		this.setAdditivity = function(additivity) {
  			var valueChanged = (additive != additivity);
  			additive = additivity;
  			if (valueChanged) {
  				this.invalidateAppenderCache();
  			}
  		};
  
  		// Create methods that use the appenders variable in this scope
  		this.addAppender = function(appender) {
  			if (isNull) {
  				handleError("Logger.addAppender: you may not add an appender to the null logger");
  			} else {
  				if (appender instanceof log4javascript.Appender) {
  					if (!array_contains(appenders, appender)) {
  						appenders.push(appender);
  						appender.setAddedToLogger(this);
  						this.invalidateAppenderCache();
  					}
  				} else {
  					handleError("Logger.addAppender: appender supplied ('" +
  						toStr(appender) + "') is not a subclass of Appender");
  				}
  			}
  		};
  
  		this.removeAppender = function(appender) {
  			array_remove(appenders, appender);
  			appender.setRemovedFromLogger(this);
  			this.invalidateAppenderCache();
  		};
  
  		this.removeAllAppenders = function() {
  			var appenderCount = appenders.length;
  			if (appenderCount > 0) {
  				for (var i = 0; i < appenderCount; i++) {
  					appenders[i].setRemovedFromLogger(this);
  				}
  				appenders.length = 0;
  				this.invalidateAppenderCache();
  			}
  		};
  
  		this.getEffectiveAppenders = function() {
  			if (appenderCache === null || appenderCacheInvalidated) {
  				// Build appender cache
  				var parentEffectiveAppenders = (isRoot || !this.getAdditivity()) ?
  					[] : this.parent.getEffectiveAppenders();
  				appenderCache = parentEffectiveAppenders.concat(appenders);
  				appenderCacheInvalidated = false;
  			}
  			return appenderCache;
  		};
  		
  		this.invalidateAppenderCache = function() {
  			appenderCacheInvalidated = true;
  			for (var i = 0, len = this.children.length; i < len; i++) {
  				this.children[i].invalidateAppenderCache();
  			}
  		};
  
  		this.log = function(level, params) {
              if(!(level instanceof Level)) {
                  this.log(Level.INFO, arguments);
                  return;
              }
  
  			if (enabled && level.isGreaterOrEqual(this.getEffectiveLevel())) {
  				// Check whether last param is an exception
  				var exception;
  				var finalParamIndex = params.length - 1;
  				var lastParam = params[finalParamIndex];
  				if (params.length > 1 && isError(lastParam)) {
  					exception = lastParam;
  					finalParamIndex--;
  				}
  
  				// Construct genuine array for the params
  				var messages = [];
  				for (var i = 0; i <= finalParamIndex; i++) {
  					messages[i] = params[i];
  				}
  
  				var loggingEvent = new LoggingEvent(
  					this, new Date(), level, messages, exception);
  
  				this.callAppenders(loggingEvent);
  			}
  		};
  
  		this.callAppenders = function(loggingEvent) {
  			var effectiveAppenders = this.getEffectiveAppenders();
  			for (var i = 0, len = effectiveAppenders.length; i < len; i++) {
  				effectiveAppenders[i].doAppend(loggingEvent);
  			}
  		};
  
  		this.setLevel = function(level) {
  			// Having a level of null on the root logger would be very bad.
  			if (isRoot && level === null) {
  				handleError("Logger.setLevel: you cannot set the level of the root logger to null");
  			} else if (level instanceof Level) {
  				loggerLevel = level;
  			} else {
  				handleError("Logger.setLevel: level supplied to logger " +
  					this.name + " is not an instance of log4javascript.Level");
  			}
  		};
  
  		this.getLevel = function() {
  			return loggerLevel;
  		};
  
  		this.getEffectiveLevel = function() {
  			for (var logger = this; logger !== null; logger = logger.parent) {
  				var level = logger.getLevel();
  				if (level !== null) {
  					return level;
  				}
  			}
  		};
  
  		this.group = function(name, initiallyExpanded) {
  			if (enabled) {
  				var effectiveAppenders = this.getEffectiveAppenders();
  				for (var i = 0, len = effectiveAppenders.length; i < len; i++) {
  					effectiveAppenders[i].group(name, initiallyExpanded);
  				}
  			}
  		};
  
  		this.groupEnd = function() {
  			if (enabled) {
  				var effectiveAppenders = this.getEffectiveAppenders();
  				for (var i = 0, len = effectiveAppenders.length; i < len; i++) {
  					effectiveAppenders[i].groupEnd();
  				}
  			}
  		};
  
  		var timers = {};
  
  		this.time = function(name, level) {
  			if (enabled) {
  				if (isUndefined(name)) {
  					handleError("Logger.time: a name for the timer must be supplied");
  				} else if (level && !(level instanceof Level)) {
  					handleError("Logger.time: level supplied to timer " +
  						name + " is not an instance of log4javascript.Level");
  				} else {
  					timers[name] = new Timer(name, level);
  				}
  			}
  		};
  
  		this.timeEnd = function(name) {
  			if (enabled) {
  				if (isUndefined(name)) {
  					handleError("Logger.timeEnd: a name for the timer must be supplied");
  				} else if (timers[name]) {
  					var timer = timers[name];
  					var milliseconds = timer.getElapsedTime();
  					this.log(timer.level, ["Timer " + toStr(name) + " completed in " + milliseconds + "ms"]);
  					delete timers[name];
  				} else {
  					logLog.warn("Logger.timeEnd: no timer found with name " + name);
  				}
  			}
  		};
  
  		this.assert = function(expr) {
  			if (enabled && !expr) {
  				var args = [];
  				for (var i = 1, len = arguments.length; i < len; i++) {
  					args.push(arguments[i]);
  				}
  				args = (args.length > 0) ? args : ["Assertion Failure"];
  				args.push(newLine);
  				args.push(expr);
  				this.log(Level.ERROR, args);
  			}
  		};
  
  		this.toString = function() {
  			return "Logger[" + this.name + "]";
  		};
  	}
  
  	Logger.prototype = {
  		trace: function() {
  			this.log(Level.TRACE, arguments);
  		},
  
  		debug: function() {
  			this.log(Level.DEBUG, arguments);
  		},
  
  		info: function() {
  			this.log(Level.INFO, arguments);
  		},
  
  		warn: function() {
  			this.log(Level.WARN, arguments);
  		},
  
  		error: function() {
  			this.log(Level.ERROR, arguments);
  		},
  
  		fatal: function() {
  			this.log(Level.FATAL, arguments);
  		},
  
  		isEnabledFor: function(level) {
  			return level.isGreaterOrEqual(this.getEffectiveLevel());
  		},
  
  		isTraceEnabled: function() {
  			return this.isEnabledFor(Level.TRACE);
  		},
  
  		isDebugEnabled: function() {
  			return this.isEnabledFor(Level.DEBUG);
  		},
  
  		isInfoEnabled: function() {
  			return this.isEnabledFor(Level.INFO);
  		},
  
  		isWarnEnabled: function() {
  			return this.isEnabledFor(Level.WARN);
  		},
  
  		isErrorEnabled: function() {
  			return this.isEnabledFor(Level.ERROR);
  		},
  
  		isFatalEnabled: function() {
  			return this.isEnabledFor(Level.FATAL);
  		}
  	};
  
  	Logger.prototype.trace.isEntryPoint = true;
  	Logger.prototype.debug.isEntryPoint = true;
  	Logger.prototype.info.isEntryPoint = true;
  	Logger.prototype.warn.isEntryPoint = true;
  	Logger.prototype.error.isEntryPoint = true;
  	Logger.prototype.fatal.isEntryPoint = true;
  
  	/* ---------------------------------------------------------------------- */
  	// Logger access methods
  
  	// Hashtable of loggers keyed by logger name
  	var loggers = {};
  	var loggerNames = [];
  
  	var ROOT_LOGGER_DEFAULT_LEVEL = Level.DEBUG;
  	var rootLogger = new Logger(rootLoggerName);
  	rootLogger.setLevel(ROOT_LOGGER_DEFAULT_LEVEL);
  
  	log4javascript.getRootLogger = function() {
  		return rootLogger;
  	};
  
  	log4javascript.getLogger = function(loggerName) {
  		// Use default logger if loggerName is not specified or invalid
  		if (typeof loggerName != "string") {
  			loggerName = anonymousLoggerName;
  			logLog.warn("log4javascript.getLogger: non-string logger name "	+
  				toStr(loggerName) + " supplied, returning anonymous logger");
  		}
  
  		// Do not allow retrieval of the root logger by name
  		if (loggerName == rootLoggerName) {
  			handleError("log4javascript.getLogger: root logger may not be obtained by name");
  		}
  
  		// Create the logger for this name if it doesn't already exist
  		if (!loggers[loggerName]) {
  			var logger = new Logger(loggerName);
  			loggers[loggerName] = logger;
  			loggerNames.push(loggerName);
  
  			// Set up parent logger, if it doesn't exist
  			var lastDotIndex = loggerName.lastIndexOf(".");
  			var parentLogger;
  			if (lastDotIndex > -1) {
  				var parentLoggerName = loggerName.substring(0, lastDotIndex);
  				parentLogger = log4javascript.getLogger(parentLoggerName); // Recursively sets up grandparents etc.
  			} else {
  				parentLogger = rootLogger;
  			}
  			parentLogger.addChild(logger);
  		}
  		return loggers[loggerName];
  	};
  
  	var defaultLogger = null;
  	log4javascript.getDefaultLogger = function() {
  		if (!defaultLogger) {
  			defaultLogger = createDefaultLogger();
  		}
  		return defaultLogger;
  	};
  
  	var nullLogger = null;
  	log4javascript.getNullLogger = function() {
  		if (!nullLogger) {
  			nullLogger = new Logger(nullLoggerName);
  			nullLogger.setLevel(Level.OFF);
  		}
  		return nullLogger;
  	};
  
  	// Destroys all loggers
  	log4javascript.resetConfiguration = function() {
  		rootLogger.setLevel(ROOT_LOGGER_DEFAULT_LEVEL);
  		loggers = {};
  	};
  
  	/* ---------------------------------------------------------------------- */
  	// Logging events
  
  	var LoggingEvent = function(logger, timeStamp, level, messages,
  			exception) {
  		this.logger = logger;
  		this.timeStamp = timeStamp;
  		this.timeStampInMilliseconds = timeStamp.getTime();
  		this.timeStampInSeconds = Math.floor(this.timeStampInMilliseconds / 1000);
  		this.milliseconds = this.timeStamp.getMilliseconds();
  		this.level = level;
  		this.messages = messages;
  		this.exception = exception;
  	};
  
  	LoggingEvent.prototype = {
  		getThrowableStrRep: function() {
  			return this.exception ?
  				getExceptionStringRep(this.exception) : "";
  		},
  		getCombinedMessages: function() {
  			return (this.messages.length == 1) ? this.messages[0] :
  				   this.messages.join(newLine);
  		},
  		toString: function() {
  			return "LoggingEvent[" + this.level + "]";
  		}
  	};
  
  	log4javascript.LoggingEvent = LoggingEvent;
  
  	/* ---------------------------------------------------------------------- */
  	// Layout prototype
  
  	var Layout = function() {
  	};
  
  	Layout.prototype = {
  		defaults: {
  			loggerKey: "logger",
  			timeStampKey: "timestamp",
  			millisecondsKey: "milliseconds",
  			levelKey: "level",
  			messageKey: "message",
  			exceptionKey: "exception",
  			urlKey: "url"
  		},
  		loggerKey: "logger",
  		timeStampKey: "timestamp",
  		millisecondsKey: "milliseconds",
  		levelKey: "level",
  		messageKey: "message",
  		exceptionKey: "exception",
  		urlKey: "url",
  		batchHeader: "",
  		batchFooter: "",
  		batchSeparator: "",
  		returnsPostData: false,
  		overrideTimeStampsSetting: false,
  		useTimeStampsInMilliseconds: null,
  
  		format: function() {
  			handleError("Layout.format: layout supplied has no format() method");
  		},
  
  		ignoresThrowable: function() {
  			handleError("Layout.ignoresThrowable: layout supplied has no ignoresThrowable() method");
  		},
  
  		getContentType: function() {
  			return "text/plain";
  		},
  
  		allowBatching: function() {
  			return true;
  		},
  
  		setTimeStampsInMilliseconds: function(timeStampsInMilliseconds) {
  			this.overrideTimeStampsSetting = true;
  			this.useTimeStampsInMilliseconds = bool(timeStampsInMilliseconds);
  		},
  
  		isTimeStampsInMilliseconds: function() {
  			return this.overrideTimeStampsSetting ?
  				this.useTimeStampsInMilliseconds : useTimeStampsInMilliseconds;
  		},
  
  		getTimeStampValue: function(loggingEvent) {
  			return this.isTimeStampsInMilliseconds() ?
  				loggingEvent.timeStampInMilliseconds : loggingEvent.timeStampInSeconds;
  		},
  
  		getDataValues: function(loggingEvent, combineMessages) {
  			var dataValues = [
  				[this.loggerKey, loggingEvent.logger.name],
  				[this.timeStampKey, this.getTimeStampValue(loggingEvent)],
  				[this.levelKey, loggingEvent.level.name],
  				[this.urlKey, window.location.href],
  				[this.messageKey, combineMessages ? loggingEvent.getCombinedMessages() : loggingEvent.messages]
  			];
  			if (!this.isTimeStampsInMilliseconds()) {
  				dataValues.push([this.millisecondsKey, loggingEvent.milliseconds]);
  			}
  			if (loggingEvent.exception) {
  				dataValues.push([this.exceptionKey, getExceptionStringRep(loggingEvent.exception)]);
  			}
  			if (this.hasCustomFields()) {
  				for (var i = 0, len = this.customFields.length; i < len; i++) {
  					var val = this.customFields[i].value;
  
  					// Check if the value is a function. If so, execute it, passing it the
  					// current layout and the logging event
  					if (typeof val === "function") {
  						val = val(this, loggingEvent);
  					}
  					dataValues.push([this.customFields[i].name, val]);
  				}
  			}
  			return dataValues;
  		},
  
  		setKeys: function(loggerKey, timeStampKey, levelKey, messageKey,
  				exceptionKey, urlKey, millisecondsKey) {
  			this.loggerKey = extractStringFromParam(loggerKey, this.defaults.loggerKey);
  			this.timeStampKey = extractStringFromParam(timeStampKey, this.defaults.timeStampKey);
  			this.levelKey = extractStringFromParam(levelKey, this.defaults.levelKey);
  			this.messageKey = extractStringFromParam(messageKey, this.defaults.messageKey);
  			this.exceptionKey = extractStringFromParam(exceptionKey, this.defaults.exceptionKey);
  			this.urlKey = extractStringFromParam(urlKey, this.defaults.urlKey);
  			this.millisecondsKey = extractStringFromParam(millisecondsKey, this.defaults.millisecondsKey);
  		},
  
  		setCustomField: function(name, value) {
  			var fieldUpdated = false;
  			for (var i = 0, len = this.customFields.length; i < len; i++) {
  				if (this.customFields[i].name === name) {
  					this.customFields[i].value = value;
  					fieldUpdated = true;
  				}
  			}
  			if (!fieldUpdated) {
  				this.customFields.push({"name": name, "value": value});
  			}
  		},
  
  		hasCustomFields: function() {
  			return (this.customFields.length > 0);
  		},
  
  		formatWithException: function(loggingEvent) {
  			var formatted = this.format(loggingEvent);
  			if (loggingEvent.exception && this.ignoresThrowable()) {
  				formatted += loggingEvent.getThrowableStrRep();
  			}
  			return formatted;
  		},
  
  		toString: function() {
  			handleError("Layout.toString: all layouts must override this method");
  		}
  	};
  
  	log4javascript.Layout = Layout;
  
  	/* ---------------------------------------------------------------------- */
  	// Appender prototype
  
  	var Appender = function() {};
  
  	Appender.prototype = new EventSupport();
  
  	Appender.prototype.layout = new PatternLayout();
  	Appender.prototype.threshold = Level.ALL;
  	Appender.prototype.loggers = [];
  
  	// Performs threshold checks before delegating actual logging to the
  	// subclass's specific append method.
  	Appender.prototype.doAppend = function(loggingEvent) {
  		if (enabled && loggingEvent.level.level >= this.threshold.level) {
  			this.append(loggingEvent);
  		}
  	};
  
  	Appender.prototype.append = function(loggingEvent) {};
  
  	Appender.prototype.setLayout = function(layout) {
  		if (layout instanceof Layout) {
  			this.layout = layout;
  		} else {
  			handleError("Appender.setLayout: layout supplied to " +
  				this.toString() + " is not a subclass of Layout");
  		}
  	};
  
  	Appender.prototype.getLayout = function() {
  		return this.layout;
  	};
  
  	Appender.prototype.setThreshold = function(threshold) {
  		if (threshold instanceof Level) {
  			this.threshold = threshold;
  		} else {
  			handleError("Appender.setThreshold: threshold supplied to " +
  				this.toString() + " is not a subclass of Level");
  		}
  	};
  
  	Appender.prototype.getThreshold = function() {
  		return this.threshold;
  	};
  
  	Appender.prototype.setAddedToLogger = function(logger) {
  		this.loggers.push(logger);
  	};
  
  	Appender.prototype.setRemovedFromLogger = function(logger) {
  		array_remove(this.loggers, logger);
  	};
  
  	Appender.prototype.group = emptyFunction;
  	Appender.prototype.groupEnd = emptyFunction;
  
  	Appender.prototype.toString = function() {
  		handleError("Appender.toString: all appenders must override this method");
  	};
  
  	log4javascript.Appender = Appender;
  
  	/* ---------------------------------------------------------------------- */
  	// SimpleLayout 
  
  	function SimpleLayout() {
  		this.customFields = [];
  	}
  
  	SimpleLayout.prototype = new Layout();
  
  	SimpleLayout.prototype.format = function(loggingEvent) {
  		return loggingEvent.level.name + " - " + loggingEvent.getCombinedMessages();
  	};
  
  	SimpleLayout.prototype.ignoresThrowable = function() {
  	    return true;
  	};
  
  	SimpleLayout.prototype.toString = function() {
  	    return "SimpleLayout";
  	};
  
  	log4javascript.SimpleLayout = SimpleLayout;
  	/* ----------------------------------------------------------------------- */
  	// NullLayout 
  
  	function NullLayout() {
  		this.customFields = [];
  	}
  
  	NullLayout.prototype = new Layout();
  
  	NullLayout.prototype.format = function(loggingEvent) {
  		return loggingEvent.messages;
  	};
  
  	NullLayout.prototype.ignoresThrowable = function() {
  	    return true;
  	};
  
  	NullLayout.prototype.formatWithException = function(loggingEvent) {
  		var messages = loggingEvent.messages, ex = loggingEvent.exception;
  		return ex ? messages.concat([ex]) : messages;
  	};
  
  	NullLayout.prototype.toString = function() {
  	    return "NullLayout";
  	};
  
  	log4javascript.NullLayout = NullLayout;
  /* ---------------------------------------------------------------------- */
  	// XmlLayout
  
  	function XmlLayout(combineMessages) {
  		this.combineMessages = extractBooleanFromParam(combineMessages, true);
  		this.customFields = [];
  	}
  
  	XmlLayout.prototype = new Layout();
  
  	XmlLayout.prototype.isCombinedMessages = function() {
  		return this.combineMessages;
  	};
  
  	XmlLayout.prototype.getContentType = function() {
  		return "text/xml";
  	};
  
  	XmlLayout.prototype.escapeCdata = function(str) {
  		return str.replace(/\]\]>/, "]]>]]&gt;<![CDATA[");
  	};
  
  	XmlLayout.prototype.format = function(loggingEvent) {
  		var layout = this;
  		var i, len;
  		function formatMessage(message) {
  			message = (typeof message === "string") ? message : toStr(message);
  			return "<log4javascript:message><![CDATA[" +
  				layout.escapeCdata(message) + "]]></log4javascript:message>";
  		}
  
  		var str = "<log4javascript:event logger=\"" + loggingEvent.logger.name +
  			"\" timestamp=\"" + this.getTimeStampValue(loggingEvent) + "\"";
  		if (!this.isTimeStampsInMilliseconds()) {
  			str += " milliseconds=\"" + loggingEvent.milliseconds + "\"";
  		}
  		str += " level=\"" + loggingEvent.level.name + "\">" + newLine;
  		if (this.combineMessages) {
  			str += formatMessage(loggingEvent.getCombinedMessages());
  		} else {
  			str += "<log4javascript:messages>" + newLine;
  			for (i = 0, len = loggingEvent.messages.length; i < len; i++) {
  				str += formatMessage(loggingEvent.messages[i]) + newLine;
  			}
  			str += "</log4javascript:messages>" + newLine;
  		}
  		if (this.hasCustomFields()) {
  			for (i = 0, len = this.customFields.length; i < len; i++) {
  				str += "<log4javascript:customfield name=\"" +
  					this.customFields[i].name + "\"><![CDATA[" +
  					this.customFields[i].value.toString() +
  					"]]></log4javascript:customfield>" + newLine;
  			}
  		}
  		if (loggingEvent.exception) {
  			str += "<log4javascript:exception><![CDATA[" +
  				getExceptionStringRep(loggingEvent.exception) +
  				"]]></log4javascript:exception>" + newLine;
  		}
  		str += "</log4javascript:event>" + newLine + newLine;
  		return str;
  	};
  
  	XmlLayout.prototype.ignoresThrowable = function() {
  	    return false;
  	};
  
  	XmlLayout.prototype.toString = function() {
  	    return "XmlLayout";
  	};
  
  	log4javascript.XmlLayout = XmlLayout;
  	/* ---------------------------------------------------------------------- */
  	// JsonLayout related
  
  	function escapeNewLines(str) {
  		return str.replace(/\r\n|\r|\n/g, "\\r\\n");
  	}
  
  	function JsonLayout(readable, combineMessages) {
  		this.readable = extractBooleanFromParam(readable, false);
  		this.combineMessages = extractBooleanFromParam(combineMessages, true);
  		this.batchHeader = this.readable ? "[" + newLine : "[";
  		this.batchFooter = this.readable ? "]" + newLine : "]";
  		this.batchSeparator = this.readable ? "," + newLine : ",";
  		this.setKeys();
  		this.colon = this.readable ? ": " : ":";
  		this.tab = this.readable ? "\t" : "";
  		this.lineBreak = this.readable ? newLine : "";
  		this.customFields = [];
  	}
  
  	/* ---------------------------------------------------------------------- */
  	// JsonLayout
  
  	JsonLayout.prototype = new Layout();
  
  	JsonLayout.prototype.isReadable = function() {
  		return this.readable;
  	};
  
  	JsonLayout.prototype.isCombinedMessages = function() {
  		return this.combineMessages;
  	};
  
      JsonLayout.prototype.format = function(loggingEvent) {
          var layout = this;
          var dataValues = this.getDataValues(loggingEvent, this.combineMessages);
          var str = "{" + this.lineBreak;
          var i, len;
  
          function formatValue(val, prefix, expand) {
              // Check the type of the data value to decide whether quotation marks
              // or expansion are required
              var formattedValue;
              var valType = typeof val;
              if (val instanceof Date) {
                  formattedValue = String(val.getTime());
              } else if (expand && (val instanceof Array)) {
                  formattedValue = "[" + layout.lineBreak;
                  for (var i = 0, len = val.length; i < len; i++) {
                      var childPrefix = prefix + layout.tab;
                      formattedValue += childPrefix + formatValue(val[i], childPrefix, false);
                      if (i < val.length - 1) {
                          formattedValue += ",";
                      }
                      formattedValue += layout.lineBreak;
                  }
                  formattedValue += prefix + "]";
              } else if (valType !== "number" && valType !== "boolean") {
                  formattedValue = "\"" + escapeNewLines(toStr(val).replace(/\"/g, "\\\"")) + "\"";
              } else {
                  formattedValue = val;
              }
              return formattedValue;
          }
  
          for (i = 0, len = dataValues.length - 1; i <= len; i++) {
              str += this.tab + "\"" + dataValues[i][0] + "\"" + this.colon + formatValue(dataValues[i][1], this.tab, true);
              if (i < len) {
                  str += ",";
              }
              str += this.lineBreak;
          }
  
          str += "}" + this.lineBreak;
          return str;
      };
  
  	JsonLayout.prototype.ignoresThrowable = function() {
  	    return false;
  	};
  
  	JsonLayout.prototype.toString = function() {
  	    return "JsonLayout";
  	};
  
  	JsonLayout.prototype.getContentType = function() {
  		return "application/json";
  	};
  
  	log4javascript.JsonLayout = JsonLayout;
  	/* ---------------------------------------------------------------------- */
  	// HttpPostDataLayout
  
  	function HttpPostDataLayout() {
  		this.setKeys();
  		this.customFields = [];
  		this.returnsPostData = true;
  	}
  
  	HttpPostDataLayout.prototype = new Layout();
  
  	// Disable batching
  	HttpPostDataLayout.prototype.allowBatching = function() {
  		return false;
  	};
  
  	HttpPostDataLayout.prototype.format = function(loggingEvent) {
  		var dataValues = this.getDataValues(loggingEvent);
  		var queryBits = [];
  		for (var i = 0, len = dataValues.length; i < len; i++) {
  			var val = (dataValues[i][1] instanceof Date) ?
  				String(dataValues[i][1].getTime()) : dataValues[i][1];
  			queryBits.push(urlEncode(dataValues[i][0]) + "=" + urlEncode(val));
  		}
  		return queryBits.join("&");
  	};
  
  	HttpPostDataLayout.prototype.ignoresThrowable = function(loggingEvent) {
  	    return false;
  	};
  
  	HttpPostDataLayout.prototype.toString = function() {
  	    return "HttpPostDataLayout";
  	};
  
  	log4javascript.HttpPostDataLayout = HttpPostDataLayout;
  	/* ---------------------------------------------------------------------- */
  	// formatObjectExpansion
  
  	function formatObjectExpansion(obj, depth, indentation) {
  		var objectsExpanded = [];
  
  		function doFormat(obj, depth, indentation) {
  			var i, len, childDepth, childIndentation, childLines, expansion,
  				childExpansion;
  
  			if (!indentation) {
  				indentation = "";
  			}
  
  			function formatString(text) {
  				var lines = splitIntoLines(text);
  				for (var j = 1, jLen = lines.length; j < jLen; j++) {
  					lines[j] = indentation + lines[j];
  				}
  				return lines.join(newLine);
  			}
  
  			if (obj === null) {
  				return "null";
  			} else if (typeof obj == "undefined") {
  				return "undefined";
  			} else if (typeof obj == "string") {
  				return formatString(obj);
  			} else if (typeof obj == "object" && array_contains(objectsExpanded, obj)) {
  				try {
  					expansion = toStr(obj);
  				} catch (ex) {
  					expansion = "Error formatting property. Details: " + getExceptionStringRep(ex);
  				}
  				return expansion + " [already expanded]";
  			} else if ((obj instanceof Array) && depth > 0) {
  				objectsExpanded.push(obj);
  				expansion = "[" + newLine;
  				childDepth = depth - 1;
  				childIndentation = indentation + "  ";
  				childLines = [];
  				for (i = 0, len = obj.length; i < len; i++) {
  					try {
  						childExpansion = doFormat(obj[i], childDepth, childIndentation);
  						childLines.push(childIndentation + childExpansion);
  					} catch (ex) {
  						childLines.push(childIndentation + "Error formatting array member. Details: " +
  							getExceptionStringRep(ex) + "");
  					}
  				}
  				expansion += childLines.join("," + newLine) + newLine + indentation + "]";
  				return expansion;
              } else if (Object.prototype.toString.call(obj) == "[object Date]") {
                  return obj.toString();
  			} else if (typeof obj == "object" && depth > 0) {
  				objectsExpanded.push(obj);
  				expansion = "{" + newLine;
  				childDepth = depth - 1;
  				childIndentation = indentation + "  ";
  				childLines = [];
  				for (i in obj) {
  					try {
  						childExpansion = doFormat(obj[i], childDepth, childIndentation);
  						childLines.push(childIndentation + i + ": " + childExpansion);
  					} catch (ex) {
  						childLines.push(childIndentation + i + ": Error formatting property. Details: " +
  							getExceptionStringRep(ex));
  					}
  				}
  				expansion += childLines.join("," + newLine) + newLine + indentation + "}";
  				return expansion;
  			} else {
  				return formatString(toStr(obj));
  			}
  		}
  		return doFormat(obj, depth, indentation);
  	}
  	/* ---------------------------------------------------------------------- */
  	// Date-related stuff
  
  	var SimpleDateFormat;
  
  	(function() {
  		var regex = /('[^']*')|(G+|y+|M+|w+|W+|D+|d+|F+|E+|a+|H+|k+|K+|h+|m+|s+|S+|Z+)|([a-zA-Z]+)|([^a-zA-Z']+)/;
  		var monthNames = ["January", "February", "March", "April", "May", "June",
  			"July", "August", "September", "October", "November", "December"];
  		var dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
  		var TEXT2 = 0, TEXT3 = 1, NUMBER = 2, YEAR = 3, MONTH = 4, TIMEZONE = 5;
  		var types = {
  			G : TEXT2,
  			y : YEAR,
  			M : MONTH,
  			w : NUMBER,
  			W : NUMBER,
  			D : NUMBER,
  			d : NUMBER,
  			F : NUMBER,
  			E : TEXT3,
  			a : TEXT2,
  			H : NUMBER,
  			k : NUMBER,
  			K : NUMBER,
  			h : NUMBER,
  			m : NUMBER,
  			s : NUMBER,
  			S : NUMBER,
  			Z : TIMEZONE
  		};
  		var ONE_DAY = 24 * 60 * 60 * 1000;
  		var ONE_WEEK = 7 * ONE_DAY;
  		var DEFAULT_MINIMAL_DAYS_IN_FIRST_WEEK = 1;
  
  		var newDateAtMidnight = function(year, month, day) {
  			var d = new Date(year, month, day, 0, 0, 0);
  			d.setMilliseconds(0);
  			return d;
  		};
  
  		Date.prototype.getDifference = function(date) {
  			return this.getTime() - date.getTime();
  		};
  
  		Date.prototype.isBefore = function(d) {
  			return this.getTime() < d.getTime();
  		};
  
  		Date.prototype.getUTCTime = function() {
  			return Date.UTC(this.getFullYear(), this.getMonth(), this.getDate(), this.getHours(), this.getMinutes(),
  					this.getSeconds(), this.getMilliseconds());
  		};
  
  		Date.prototype.getTimeSince = function(d) {
  			return this.getUTCTime() - d.getUTCTime();
  		};
  
  		Date.prototype.getPreviousSunday = function() {
  			// Using midday avoids any possibility of DST messing things up
  			var midday = new Date(this.getFullYear(), this.getMonth(), this.getDate(), 12, 0, 0);
  			var previousSunday = new Date(midday.getTime() - this.getDay() * ONE_DAY);
  			return newDateAtMidnight(previousSunday.getFullYear(), previousSunday.getMonth(),
  					previousSunday.getDate());
  		};
  
  		Date.prototype.getWeekInYear = function(minimalDaysInFirstWeek) {
  			if (isUndefined(this.minimalDaysInFirstWeek)) {
  				minimalDaysInFirstWeek = DEFAULT_MINIMAL_DAYS_IN_FIRST_WEEK;
  			}
  			var previousSunday = this.getPreviousSunday();
  			var startOfYear = newDateAtMidnight(this.getFullYear(), 0, 1);
  			var numberOfSundays = previousSunday.isBefore(startOfYear) ?
  				0 : 1 + Math.floor(previousSunday.getTimeSince(startOfYear) / ONE_WEEK);
  			var numberOfDaysInFirstWeek =  7 - startOfYear.getDay();
  			var weekInYear = numberOfSundays;
  			if (numberOfDaysInFirstWeek < minimalDaysInFirstWeek) {
  				weekInYear--;
  			}
  			return weekInYear;
  		};
  
  		Date.prototype.getWeekInMonth = function(minimalDaysInFirstWeek) {
  			if (isUndefined(this.minimalDaysInFirstWeek)) {
  				minimalDaysInFirstWeek = DEFAULT_MINIMAL_DAYS_IN_FIRST_WEEK;
  			}
  			var previousSunday = this.getPreviousSunday();
  			var startOfMonth = newDateAtMidnight(this.getFullYear(), this.getMonth(), 1);
  			var numberOfSundays = previousSunday.isBefore(startOfMonth) ?
  				0 : 1 + Math.floor(previousSunday.getTimeSince(startOfMonth) / ONE_WEEK);
  			var numberOfDaysInFirstWeek =  7 - startOfMonth.getDay();
  			var weekInMonth = numberOfSundays;
  			if (numberOfDaysInFirstWeek >= minimalDaysInFirstWeek) {
  				weekInMonth++;
  			}
  			return weekInMonth;
  		};
  
  		Date.prototype.getDayInYear = function() {
  			var startOfYear = newDateAtMidnight(this.getFullYear(), 0, 1);
  			return 1 + Math.floor(this.getTimeSince(startOfYear) / ONE_DAY);
  		};
  
  		/* ------------------------------------------------------------------ */
  
  		SimpleDateFormat = function(formatString) {
  			this.formatString = formatString;
  		};
  
  		/**
  		 * Sets the minimum number of days in a week in order for that week to
  		 * be considered as belonging to a particular month or year
  		 */
  		SimpleDateFormat.prototype.setMinimalDaysInFirstWeek = function(days) {
  			this.minimalDaysInFirstWeek = days;
  		};
  
  		SimpleDateFormat.prototype.getMinimalDaysInFirstWeek = function() {
  			return isUndefined(this.minimalDaysInFirstWeek)	?
  				DEFAULT_MINIMAL_DAYS_IN_FIRST_WEEK : this.minimalDaysInFirstWeek;
  		};
  
  		var padWithZeroes = function(str, len) {
  			while (str.length < len) {
  				str = "0" + str;
  			}
  			return str;
  		};
  
  		var formatText = function(data, numberOfLetters, minLength) {
  			return (numberOfLetters >= 4) ? data : data.substr(0, Math.max(minLength, numberOfLetters));
  		};
  
  		var formatNumber = function(data, numberOfLetters) {
  			var dataString = "" + data;
  			// Pad with 0s as necessary
  			return padWithZeroes(dataString, numberOfLetters);
  		};
  
  		SimpleDateFormat.prototype.format = function(date) {
  			var formattedString = "";
  			var result;
  			var searchString = this.formatString;
  			while ((result = regex.exec(searchString))) {
  				var quotedString = result[1];
  				var patternLetters = result[2];
  				var otherLetters = result[3];
  				var otherCharacters = result[4];
  
  				// If the pattern matched is quoted string, output the text between the quotes
  				if (quotedString) {
  					if (quotedString == "''") {
  						formattedString += "'";
  					} else {
  						formattedString += quotedString.substring(1, quotedString.length - 1);
  					}
  				} else if (otherLetters) {
  					// Swallow non-pattern letters by doing nothing here
  				} else if (otherCharacters) {
  					// Simply output other characters
  					formattedString += otherCharacters;
  				} else if (patternLetters) {
  					// Replace pattern letters
  					var patternLetter = patternLetters.charAt(0);
  					var numberOfLetters = patternLetters.length;
  					var rawData = "";
  					switch(patternLetter) {
  						case "G":
  							rawData = "AD";
  							break;
  						case "y":
  							rawData = date.getFullYear();
  							break;
  						case "M":
  							rawData = date.getMonth();
  							break;
  						case "w":
  							rawData = date.getWeekInYear(this.getMinimalDaysInFirstWeek());
  							break;
  						case "W":
  							rawData = date.getWeekInMonth(this.getMinimalDaysInFirstWeek());
  							break;
  						case "D":
  							rawData = date.getDayInYear();
  							break;
  						case "d":
  							rawData = date.getDate();
  							break;
  						case "F":
  							rawData = 1 + Math.floor((date.getDate() - 1) / 7);
  							break;
  						case "E":
  							rawData = dayNames[date.getDay()];
  							break;
  						case "a":
  							rawData = (date.getHours() >= 12) ? "PM" : "AM";
  							break;
  						case "H":
  							rawData = date.getHours();
  							break;
  						case "k":
  							rawData = date.getHours() || 24;
  							break;
  						case "K":
  							rawData = date.getHours() % 12;
  							break;
  						case "h":
  							rawData = (date.getHours() % 12) || 12;
  							break;
  						case "m":
  							rawData = date.getMinutes();
  							break;
  						case "s":
  							rawData = date.getSeconds();
  							break;
  						case "S":
  							rawData = date.getMilliseconds();
  							break;
  						case "Z":
  							rawData = date.getTimezoneOffset(); // This returns the number of minutes since GMT was this time.
  							break;
  					}
  					// Format the raw data depending on the type
  					switch(types[patternLetter]) {
  						case TEXT2:
  							formattedString += formatText(rawData, numberOfLetters, 2);
  							break;
  						case TEXT3:
  							formattedString += formatText(rawData, numberOfLetters, 3);
  							break;
  						case NUMBER:
  							formattedString += formatNumber(rawData, numberOfLetters);
  							break;
  						case YEAR:
  							if (numberOfLetters <= 3) {
  								// Output a 2-digit year
  								var dataString = "" + rawData;
  								formattedString += dataString.substr(2, 2);
  							} else {
  								formattedString += formatNumber(rawData, numberOfLetters);
  							}
  							break;
  						case MONTH:
  							if (numberOfLetters >= 3) {
  								formattedString += formatText(monthNames[rawData], numberOfLetters, numberOfLetters);
  							} else {
  								// NB. Months returned by getMonth are zero-based
  								formattedString += formatNumber(rawData + 1, numberOfLetters);
  							}
  							break;
  						case TIMEZONE:
  							var isPositive = (rawData > 0);
  							// The following line looks like a mistake but isn't
  							// because of the way getTimezoneOffset measures.
  							var prefix = isPositive ? "-" : "+";
  							var absData = Math.abs(rawData);
  
  							// Hours
  							var hours = "" + Math.floor(absData / 60);
  							hours = padWithZeroes(hours, 2);
  							// Minutes
  							var minutes = "" + (absData % 60);
  							minutes = padWithZeroes(minutes, 2);
  
  							formattedString += prefix + hours + minutes;
  							break;
  					}
  				}
  				searchString = searchString.substr(result.index + result[0].length);
  			}
  			return formattedString;
  		};
  	})();
  
  	log4javascript.SimpleDateFormat = SimpleDateFormat;
  
  	/* ---------------------------------------------------------------------- */
  	// PatternLayout
  
  	function PatternLayout(pattern) {
  		if (pattern) {
  			this.pattern = pattern;
  		} else {
  			this.pattern = PatternLayout.DEFAULT_CONVERSION_PATTERN;
  		}
  		this.customFields = [];
  	}
  
  	PatternLayout.TTCC_CONVERSION_PATTERN = "%r %p %c - %m%n";
  	PatternLayout.DEFAULT_CONVERSION_PATTERN = "%m%n";
  	PatternLayout.ISO8601_DATEFORMAT = "yyyy-MM-dd HH:mm:ss,SSS";
  	PatternLayout.DATETIME_DATEFORMAT = "dd MMM yyyy HH:mm:ss,SSS";
  	PatternLayout.ABSOLUTETIME_DATEFORMAT = "HH:mm:ss,SSS";
  
  	PatternLayout.prototype = new Layout();
  
  	PatternLayout.prototype.format = function(loggingEvent) {
  		var regex = /%(-?[0-9]+)?(\.?[0-9]+)?([acdfmMnpr%])(\{([^\}]+)\})?|([^%]+)/;
  		var formattedString = "";
  		var result;
  		var searchString = this.pattern;
  
  		// Cannot use regex global flag since it doesn't work with exec in IE5
  		while ((result = regex.exec(searchString))) {
  			var matchedString = result[0];
  			var padding = result[1];
  			var truncation = result[2];
  			var conversionCharacter = result[3];
  			var specifier = result[5];
  			var text = result[6];
  
  			// Check if the pattern matched was just normal text
  			if (text) {
  				formattedString += "" + text;
  			} else {
  				// Create a raw replacement string based on the conversion
  				// character and specifier
  				var replacement = "";
  				switch(conversionCharacter) {
  					case "a": // Array of messages
  					case "m": // Message
  						var depth = 0;
  						if (specifier) {
  							depth = parseInt(specifier, 10);
  							if (isNaN(depth)) {
  								handleError("PatternLayout.format: invalid specifier '" +
  									specifier + "' for conversion character '" + conversionCharacter +
  									"' - should be a number");
  								depth = 0;
  							}
  						}
  						var messages = (conversionCharacter === "a") ? loggingEvent.messages[0] : loggingEvent.messages;
  						for (var i = 0, len = messages.length; i < len; i++) {
  							if (i > 0 && (replacement.charAt(replacement.length - 1) !== " ")) {
  								replacement += " ";
  							}
  							if (depth === 0) {
  								replacement += messages[i];
  							} else {
  								replacement += formatObjectExpansion(messages[i], depth);
  							}
  						}
  						break;
  					case "c": // Logger name
  						var loggerName = loggingEvent.logger.name;
  						if (specifier) {
  							var precision = parseInt(specifier, 10);
  							var loggerNameBits = loggingEvent.logger.name.split(".");
  							if (precision >= loggerNameBits.length) {
  								replacement = loggerName;
  							} else {
  								replacement = loggerNameBits.slice(loggerNameBits.length - precision).join(".");
  							}
  						} else {
  							replacement = loggerName;
  						}
  						break;
  					case "d": // Date
  						var dateFormat = PatternLayout.ISO8601_DATEFORMAT;
  						if (specifier) {
  							dateFormat = specifier;
  							// Pick up special cases
  							if (dateFormat == "ISO8601") {
  								dateFormat = PatternLayout.ISO8601_DATEFORMAT;
  							} else if (dateFormat == "ABSOLUTE") {
  								dateFormat = PatternLayout.ABSOLUTETIME_DATEFORMAT;
  							} else if (dateFormat == "DATE") {
  								dateFormat = PatternLayout.DATETIME_DATEFORMAT;
  							}
  						}
  						// Format the date
  						replacement = (new SimpleDateFormat(dateFormat)).format(loggingEvent.timeStamp);
  						break;
  					case "f": // Custom field
  						if (this.hasCustomFields()) {
  							var fieldIndex = 0;
  							if (specifier) {
  								fieldIndex = parseInt(specifier, 10);
  								if (isNaN(fieldIndex)) {
  									handleError("PatternLayout.format: invalid specifier '" +
  										specifier + "' for conversion character 'f' - should be a number");
  								} else if (fieldIndex === 0) {
  									handleError("PatternLayout.format: invalid specifier '" +
  										specifier + "' for conversion character 'f' - must be greater than zero");
  								} else if (fieldIndex > this.customFields.length) {
  									handleError("PatternLayout.format: invalid specifier '" +
  										specifier + "' for conversion character 'f' - there aren't that many custom fields");
  								} else {
  									fieldIndex = fieldIndex - 1;
  								}
  							}
                              var val = this.customFields[fieldIndex].value;
                              if (typeof val == "function") {
                                  val = val(this, loggingEvent);
                              }
                              replacement = val;
  						}
  						break;
  					case "n": // New line
  						replacement = newLine;
  						break;
  					case "p": // Level
  						replacement = loggingEvent.level.name;
  						break;
  					case "r": // Milliseconds since log4javascript startup
  						replacement = "" + loggingEvent.timeStamp.getDifference(applicationStartDate);
  						break;
  					case "%": // Literal % sign
  						replacement = "%";
  						break;
  					default:
  						replacement = matchedString;
  						break;
  				}
  				// Format the replacement according to any padding or
  				// truncation specified
  				var l;
  
  				// First, truncation
  				if (truncation) {
  					l = parseInt(truncation.substr(1), 10);
  					var strLen = replacement.length;
  					if (l < strLen) {
  						replacement = replacement.substring(strLen - l, strLen);
  					}
  				}
  				// Next, padding
  				if (padding) {
  					if (padding.charAt(0) == "-") {
  						l = parseInt(padding.substr(1), 10);
  						// Right pad with spaces
  						while (replacement.length < l) {
  							replacement += " ";
  						}
  					} else {
  						l = parseInt(padding, 10);
  						// Left pad with spaces
  						while (replacement.length < l) {
  							replacement = " " + replacement;
  						}
  					}
  				}
  				formattedString += replacement;
  			}
  			searchString = searchString.substr(result.index + result[0].length);
  		}
  		return formattedString;
  	};
  
  	PatternLayout.prototype.ignoresThrowable = function() {
  	    return true;
  	};
  
  	PatternLayout.prototype.toString = function() {
  	    return "PatternLayout";
  	};
  
  	log4javascript.PatternLayout = PatternLayout;
  	/* ---------------------------------------------------------------------- */
  	// AlertAppender
  
  	function AlertAppender() {}
  
  	AlertAppender.prototype = new Appender();
  
  	AlertAppender.prototype.layout = new SimpleLayout();
  
  	AlertAppender.prototype.append = function(loggingEvent) {
  		alert( this.getLayout().formatWithException(loggingEvent) );
  	};
  
  	AlertAppender.prototype.toString = function() {
  		return "AlertAppender";
  	};
  
  	log4javascript.AlertAppender = AlertAppender;
  	/* ---------------------------------------------------------------------- */
  	// BrowserConsoleAppender (only works in Opera and Safari and Firefox with
  	// Firebug extension)
  
  	function BrowserConsoleAppender() {}
  
  	BrowserConsoleAppender.prototype = new log4javascript.Appender();
  	BrowserConsoleAppender.prototype.layout = new NullLayout();
  	BrowserConsoleAppender.prototype.threshold = Level.DEBUG;
  
  	BrowserConsoleAppender.prototype.append = function(loggingEvent) {
  		var appender = this;
  
  		var getFormattedMessage = function() {
  			var formattedMessage = appender.getLayout().formatWithException(loggingEvent);
  			return (typeof formattedMessage == "string") ? [formattedMessage] : formattedMessage;
  		};
  
  		var console;
  
  		if ( (console = window.console) && console.log) { // Safari and Firebug
  			var formattedMessage = getFormattedMessage();
  
  			// Log to Firebug or the browser console using specific logging
  			// methods or revert to console.log otherwise 
  			var consoleMethodName;
  
  			if (console.debug && Level.DEBUG.isGreaterOrEqual(loggingEvent.level)) {
  				consoleMethodName = "debug";
  			} else if (console.info && Level.INFO.equals(loggingEvent.level)) {
  				consoleMethodName = "info";
  			} else if (console.warn && Level.WARN.equals(loggingEvent.level)) {
  				consoleMethodName = "warn";
  			} else if (console.error && loggingEvent.level.isGreaterOrEqual(Level.ERROR)) {
  				consoleMethodName = "error";
  			} else {
  				consoleMethodName = "log";
  			}
  
  			if (console[consoleMethodName].apply) {
  				console[consoleMethodName].apply(console, formattedMessage);
  			} else {
  				console[consoleMethodName](formattedMessage);
  			}
  		} else if ((typeof opera != "undefined") && opera.postError) { // Opera
  			opera.postError(getFormattedMessage());
  		}
  	};
  
  	BrowserConsoleAppender.prototype.group = function(name) {
  		if (window.console && window.console.group) {
  			window.console.group(name);
  		}
  	};
  
  	BrowserConsoleAppender.prototype.groupEnd = function() {
  		if (window.console && window.console.groupEnd) {
  			window.console.groupEnd();
  		}
  	};
  
  	BrowserConsoleAppender.prototype.toString = function() {
  		return "BrowserConsoleAppender";
  	};
  
  	log4javascript.BrowserConsoleAppender = BrowserConsoleAppender;
  	/* ---------------------------------------------------------------------- */
  	// AjaxAppender related
  
  	var xhrFactory = function() { return new XMLHttpRequest(); };
  	var xmlHttpFactories = [
  		xhrFactory,
  		function() { return new ActiveXObject("Msxml2.XMLHTTP"); },
  		function() { return new ActiveXObject("Microsoft.XMLHTTP"); }
  	];
  
  	var withCredentialsSupported = false;
  	var getXmlHttp = function(errorHandler) {
  		// This is only run the first time; the value of getXmlHttp gets
  		// replaced with the factory that succeeds on the first run
  		var xmlHttp = null, factory;
  		for (var i = 0, len = xmlHttpFactories.length; i < len; i++) {
  			factory = xmlHttpFactories[i];
  			try {
  				xmlHttp = factory();
  				withCredentialsSupported = (factory == xhrFactory && ("withCredentials" in xmlHttp));
  				getXmlHttp = factory;
  				return xmlHttp;
  			} catch (e) {
  			}
  		}
  		// If we're here, all factories have failed, so throw an error
  		if (errorHandler) {
  			errorHandler();
  		} else {
  			handleError("getXmlHttp: unable to obtain XMLHttpRequest object");
  		}
  	};
  
  	function isHttpRequestSuccessful(xmlHttp) {
  		return isUndefined(xmlHttp.status) || xmlHttp.status === 0 ||
  			(xmlHttp.status >= 200 && xmlHttp.status < 300) ||
  			xmlHttp.status == 1223 /* Fix for IE */;
  	}
  
  	/* ---------------------------------------------------------------------- */
  	// AjaxAppender
  
  	function AjaxAppender(url, withCredentials) {
  		var appender = this;
  		var isSupported = true;
  		if (!url) {
  			handleError("AjaxAppender: URL must be specified in constructor");
  			isSupported = false;
  		}
  
  		var timed = this.defaults.timed;
  		var waitForResponse = this.defaults.waitForResponse;
  		var batchSize = this.defaults.batchSize;
  		var timerInterval = this.defaults.timerInterval;
  		var requestSuccessCallback = this.defaults.requestSuccessCallback;
  		var failCallback = this.defaults.failCallback;
  		var postVarName = this.defaults.postVarName;
  		var sendAllOnUnload = this.defaults.sendAllOnUnload;
  		var contentType = this.defaults.contentType;
  		var sessionId = null;
  
  		var queuedLoggingEvents = [];
  		var queuedRequests = [];
  		var headers = [];
  		var sending = false;
  		var initialized = false;
  
  		// Configuration methods. The function scope is used to prevent
  		// direct alteration to the appender configuration properties.
  		function checkCanConfigure(configOptionName) {
  			if (initialized) {
  				handleError("AjaxAppender: configuration option '" +
  					configOptionName +
  					"' may not be set after the appender has been initialized");
  				return false;
  			}
  			return true;
  		}
  
  		this.getSessionId = function() { return sessionId; };
  		this.setSessionId = function(sessionIdParam) {
  			sessionId = extractStringFromParam(sessionIdParam, null);
  			this.layout.setCustomField("sessionid", sessionId);
  		};
  
  		this.setLayout = function(layoutParam) {
  			if (checkCanConfigure("layout")) {
  				this.layout = layoutParam;
  				// Set the session id as a custom field on the layout, if not already present
  				if (sessionId !== null) {
  					this.setSessionId(sessionId);
  				}
  			}
  		};
  
  		this.isTimed = function() { return timed; };
  		this.setTimed = function(timedParam) {
  			if (checkCanConfigure("timed")) {
  				timed = bool(timedParam);
  			}
  		};
  
  		this.getTimerInterval = function() { return timerInterval; };
  		this.setTimerInterval = function(timerIntervalParam) {
  			if (checkCanConfigure("timerInterval")) {
  				timerInterval = extractIntFromParam(timerIntervalParam, timerInterval);
  			}
  		};
  
  		this.isWaitForResponse = function() { return waitForResponse; };
  		this.setWaitForResponse = function(waitForResponseParam) {
  			if (checkCanConfigure("waitForResponse")) {
  				waitForResponse = bool(waitForResponseParam);
  			}
  		};
  
  		this.getBatchSize = function() { return batchSize; };
  		this.setBatchSize = function(batchSizeParam) {
  			if (checkCanConfigure("batchSize")) {
  				batchSize = extractIntFromParam(batchSizeParam, batchSize);
  			}
  		};
  
  		this.isSendAllOnUnload = function() { return sendAllOnUnload; };
  		this.setSendAllOnUnload = function(sendAllOnUnloadParam) {
  			if (checkCanConfigure("sendAllOnUnload")) {
  				sendAllOnUnload = extractBooleanFromParam(sendAllOnUnloadParam, sendAllOnUnload);
  			}
  		};
  
  		this.setRequestSuccessCallback = function(requestSuccessCallbackParam) {
  			requestSuccessCallback = extractFunctionFromParam(requestSuccessCallbackParam, requestSuccessCallback);
  		};
  
  		this.setFailCallback = function(failCallbackParam) {
  			failCallback = extractFunctionFromParam(failCallbackParam, failCallback);
  		};
  
  		this.getPostVarName = function() { return postVarName; };
  		this.setPostVarName = function(postVarNameParam) {
  			if (checkCanConfigure("postVarName")) {
  				postVarName = extractStringFromParam(postVarNameParam, postVarName);
  			}
  		};
  
  		this.getHeaders = function() { return headers; };
  		this.addHeader = function(name, value) {
  			if (name.toLowerCase() == "content-type") {
  				contentType = value;
  			} else {
  				headers.push( { name: name, value: value } );
  			}
  		};
  
  		// Internal functions
  		function sendAll() {
  			if (isSupported && enabled) {
  				sending = true;
  				var currentRequestBatch;
  				if (waitForResponse) {
  					// Send the first request then use this function as the callback once
  					// the response comes back
  					if (queuedRequests.length > 0) {
  						currentRequestBatch = queuedRequests.shift();
  						sendRequest(preparePostData(currentRequestBatch), sendAll);
  					} else {
  						sending = false;
  						if (timed) {
  							scheduleSending();
  						}
  					}
  				} else {
  					// Rattle off all the requests without waiting to see the response
  					while ((currentRequestBatch = queuedRequests.shift())) {
  						sendRequest(preparePostData(currentRequestBatch));
  					}
  					sending = false;
  					if (timed) {
  						scheduleSending();
  					}
  				}
  			}
  		}
  
  		this.sendAll = sendAll;
  
  		// Called when the window unloads. At this point we're past caring about
  		// waiting for responses or timers or incomplete batches - everything
  		// must go, now
  		function sendAllRemaining() {
  			var sendingAnything = false;
  			if (isSupported && enabled) {
  				// Create requests for everything left over, batched as normal
  				var actualBatchSize = appender.getLayout().allowBatching() ? batchSize : 1;
  				var currentLoggingEvent;
  				var batchedLoggingEvents = [];
  				while ((currentLoggingEvent = queuedLoggingEvents.shift())) {
  					batchedLoggingEvents.push(currentLoggingEvent);
  					if (queuedLoggingEvents.length >= actualBatchSize) {
  						// Queue this batch of log entries
  						queuedRequests.push(batchedLoggingEvents);
  						batchedLoggingEvents = [];
  					}
  				}
  				// If there's a partially completed batch, add it
  				if (batchedLoggingEvents.length > 0) {
  					queuedRequests.push(batchedLoggingEvents);
  				}
  				sendingAnything = (queuedRequests.length > 0);
  				waitForResponse = false;
  				timed = false;
  				sendAll();
  			}
  			return sendingAnything;
  		}
  
  		this.sendAllRemaining = sendAllRemaining;
  
  		function preparePostData(batchedLoggingEvents) {
  			// Format the logging events
  			var formattedMessages = [];
  			var currentLoggingEvent;
  			var postData = "";
  			while ((currentLoggingEvent = batchedLoggingEvents.shift())) {
  				formattedMessages.push( appender.getLayout().formatWithException(currentLoggingEvent) );
  			}
  			// Create the post data string
  			if (batchedLoggingEvents.length == 1) {
  				postData = formattedMessages.join("");
  			} else {
  				postData = appender.getLayout().batchHeader +
  					formattedMessages.join(appender.getLayout().batchSeparator) +
  					appender.getLayout().batchFooter;
  			}
  			if (contentType == appender.defaults.contentType) {
  				postData = appender.getLayout().returnsPostData ? postData :
  					urlEncode(postVarName) + "=" + urlEncode(postData);
  				// Add the layout name to the post data
  				if (postData.length > 0) {
  					postData += "&";
  				}
  				postData += "layout=" + urlEncode(appender.getLayout().toString());
  			}
  			return postData;
  		}
  
  		function scheduleSending() {
  			window.setTimeout(sendAll, timerInterval);
  		}
  
  		function xmlHttpErrorHandler() {
  			var msg = "AjaxAppender: could not create XMLHttpRequest object. AjaxAppender disabled";
  			handleError(msg);
  			isSupported = false;
  			if (failCallback) {
  				failCallback(msg);
  			}
  		}
  
  		function sendRequest(postData, successCallback) {
  			try {
  				var xmlHttp = getXmlHttp(xmlHttpErrorHandler);
  				if (isSupported) {
  					xmlHttp.onreadystatechange = function() {
  						if (xmlHttp.readyState == 4) {
  							if (isHttpRequestSuccessful(xmlHttp)) {
  								if (requestSuccessCallback) {
  									requestSuccessCallback(xmlHttp);
  								}
  								if (successCallback) {
  									successCallback(xmlHttp);
  								}
  							} else {
  								var msg = "AjaxAppender.append: XMLHttpRequest request to URL " +
  									url + " returned status code " + xmlHttp.status;
  								handleError(msg);
  								if (failCallback) {
  									failCallback(msg);
  								}
  							}
  							xmlHttp.onreadystatechange = emptyFunction;
  							xmlHttp = null;
  						}
  					};
  					xmlHttp.open("POST", url, true);
  					// Add withCredentials to facilitate CORS requests with cookies
  					if (withCredentials && withCredentialsSupported) {
  						xmlHttp.withCredentials = true;
  					}
  					try {
  						for (var i = 0, header; header = headers[i++]; ) {
  							xmlHttp.setRequestHeader(header.name, header.value);
  						}
  						xmlHttp.setRequestHeader("Content-Type", contentType);
  					} catch (headerEx) {
  						var msg = "AjaxAppender.append: your browser's XMLHttpRequest implementation" +
  							" does not support setRequestHeader, therefore cannot post data. AjaxAppender disabled";
  						handleError(msg);
  						isSupported = false;
  						if (failCallback) {
  							failCallback(msg);
  						}
  						return;
  					}
  					xmlHttp.send(postData);
  				}
  			} catch (ex) {
  				var errMsg = "AjaxAppender.append: error sending log message to " + url;
  				handleError(errMsg, ex);
  				isSupported = false;
  				if (failCallback) {
  					failCallback(errMsg + ". Details: " + getExceptionStringRep(ex));
  				}
  			}
  		}
  
  		this.append = function(loggingEvent) {
  			if (isSupported) {
  				if (!initialized) {
  					init();
  				}
  				queuedLoggingEvents.push(loggingEvent);
  				var actualBatchSize = this.getLayout().allowBatching() ? batchSize : 1;
  
  				if (queuedLoggingEvents.length >= actualBatchSize) {
  					var currentLoggingEvent;
  					var batchedLoggingEvents = [];
  					while ((currentLoggingEvent = queuedLoggingEvents.shift())) {
  						batchedLoggingEvents.push(currentLoggingEvent);
  					}
  					// Queue this batch of log entries
  					queuedRequests.push(batchedLoggingEvents);
  
  					// If using a timer, the queue of requests will be processed by the
  					// timer function, so nothing needs to be done here.
  					if (!timed && (!waitForResponse || (waitForResponse && !sending))) {
  						sendAll();
  					}
  				}
  			}
  		};
  
  		function init() {
  			initialized = true;
  			// Add unload event to send outstanding messages
  			if (sendAllOnUnload) {
  				var oldBeforeUnload = window.onbeforeunload;
  				window.onbeforeunload = function() {
  					if (oldBeforeUnload) {
  						oldBeforeUnload();
  					}
  					sendAllRemaining();
  				};
  			}
  			// Start timer
  			if (timed) {
  				scheduleSending();
  			}
  		}
  	}
  
  	AjaxAppender.prototype = new Appender();
  
  	AjaxAppender.prototype.defaults = {
  		waitForResponse: false,
  		timed: false,
  		timerInterval: 1000,
  		batchSize: 1,
  		sendAllOnUnload: false,
  		requestSuccessCallback: null,
  		failCallback: null,
  		postVarName: "data",
  		contentType: "application/x-www-form-urlencoded"
  	};
  
  	AjaxAppender.prototype.layout = new HttpPostDataLayout();
  
  	AjaxAppender.prototype.toString = function() {
  		return "AjaxAppender";
  	};
  
  	log4javascript.AjaxAppender = AjaxAppender;
  	/* ---------------------------------------------------------------------- */
  	// PopUpAppender and InPageAppender related
  
  	function setCookie(name, value, days, path) {
  		var expires;
  		path = path ? "; path=" + path : "";
  		if (days) {
  			var date = new Date();
  			date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
  			expires = "; expires=" + date.toGMTString();
  		} else {
  			expires = "";
  		}
  		document.cookie = escape(name) + "=" + escape(value) + expires + path;
  	}
  
  	function getCookie(name) {
  		var nameEquals = escape(name) + "=";
  		var ca = document.cookie.split(";");
  		for (var i = 0, len = ca.length; i < len; i++) {
  			var c = ca[i];
  			while (c.charAt(0) === " ") {
  				c = c.substring(1, c.length);
  			}
  			if (c.indexOf(nameEquals) === 0) {
  				return unescape(c.substring(nameEquals.length, c.length));
  			}
  		}
  		return null;
  	}
  
  	// Gets the base URL of the location of the log4javascript script.
  	// This is far from infallible.
  	function getBaseUrl() {
  		var scripts = document.getElementsByTagName("script");
  		for (var i = 0, len = scripts.length; i < len; ++i) {
  			if (scripts[i].src.indexOf("log4javascript") != -1) {
  				var lastSlash = scripts[i].src.lastIndexOf("/");
  				return (lastSlash == -1) ? "" : scripts[i].src.substr(0, lastSlash + 1);
  			}
  		}
  		return null;
  	}
  
  	function isLoaded(win) {
  		try {
  			return bool(win.loaded);
  		} catch (ex) {
  			return false;
  		}
  	}
  
  	/* ---------------------------------------------------------------------- */
  	// ConsoleAppender (prototype for PopUpAppender and InPageAppender)
  
  	var ConsoleAppender;
  
  	// Create an anonymous function to protect base console methods
  	(function() {
  		var getConsoleHtmlLines = function() {
  			return [
  '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
  '<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">',
  '	<head>',
  '		<title>log4javascript</title>',
  '		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />',
  '		<!-- Make IE8 behave like IE7, having gone to all the trouble of making IE work -->',
  '		<meta http-equiv="X-UA-Compatible" content="IE=7" />',
  '		<script type="text/javascript">var isIe = false, isIePre7 = false;</script>',
  '		<!--[if IE]><script type="text/javascript">isIe = true</script><![endif]-->',
  '		<!--[if lt IE 7]><script type="text/javascript">isIePre7 = true</script><![endif]-->',
  '		<script type="text/javascript">',
  '			//<![CDATA[',
  '			var loggingEnabled = true;',
  '			var logQueuedEventsTimer = null;',
  '			var logEntries = [];',
  '			var logEntriesAndSeparators = [];',
  '			var logItems = [];',
  '			var renderDelay = 100;',
  '			var unrenderedLogItemsExist = false;',
  '			var rootGroup, currentGroup = null;',
  '			var loaded = false;',
  '			var currentLogItem = null;',
  '			var logMainContainer;',
  '',
  '			function copyProperties(obj, props) {',
  '				for (var i in props) {',
  '					obj[i] = props[i];',
  '				}',
  '			}',
  '',
  '			/*----------------------------------------------------------------*/',
  '',
  '			function LogItem() {',
  '			}',
  '',
  '			LogItem.prototype = {',
  '				mainContainer: null,',
  '				wrappedContainer: null,',
  '				unwrappedContainer: null,',
  '				group: null,',
  '',
  '				appendToLog: function() {',
  '					for (var i = 0, len = this.elementContainers.length; i < len; i++) {',
  '						this.elementContainers[i].appendToLog();',
  '					}',
  '					this.group.update();',
  '				},',
  '',
  '				doRemove: function(doUpdate, removeFromGroup) {',
  '					if (this.rendered) {',
  '						for (var i = 0, len = this.elementContainers.length; i < len; i++) {',
  '							this.elementContainers[i].remove();',
  '						}',
  '						this.unwrappedElementContainer = null;',
  '						this.wrappedElementContainer = null;',
  '						this.mainElementContainer = null;',
  '					}',
  '					if (this.group && removeFromGroup) {',
  '						this.group.removeChild(this, doUpdate);',
  '					}',
  '					if (this === currentLogItem) {',
  '						currentLogItem = null;',
  '					}',
  '				},',
  '',
  '				remove: function(doUpdate, removeFromGroup) {',
  '					this.doRemove(doUpdate, removeFromGroup);',
  '				},',
  '',
  '				render: function() {},',
  '',
  '				accept: function(visitor) {',
  '					visitor.visit(this);',
  '				},',
  '',
  '				getUnwrappedDomContainer: function() {',
  '					return this.group.unwrappedElementContainer.contentDiv;',
  '				},',
  '',
  '				getWrappedDomContainer: function() {',
  '					return this.group.wrappedElementContainer.contentDiv;',
  '				},',
  '',
  '				getMainDomContainer: function() {',
  '					return this.group.mainElementContainer.contentDiv;',
  '				}',
  '			};',
  '',
  '			LogItem.serializedItemKeys = {LOG_ENTRY: 0, GROUP_START: 1, GROUP_END: 2};',
  '',
  '			/*----------------------------------------------------------------*/',
  '',
  '			function LogItemContainerElement() {',
  '			}',
  '',
  '			LogItemContainerElement.prototype = {',
  '				appendToLog: function() {',
  '					var insertBeforeFirst = (newestAtTop && this.containerDomNode.hasChildNodes());',
  '					if (insertBeforeFirst) {',
  '						this.containerDomNode.insertBefore(this.mainDiv, this.containerDomNode.firstChild);',
  '					} else {',
  '						this.containerDomNode.appendChild(this.mainDiv);',
  '					}',
  '				}',
  '			};',
  '',
  '			/*----------------------------------------------------------------*/',
  '',
  '			function SeparatorElementContainer(containerDomNode) {',
  '				this.containerDomNode = containerDomNode;',
  '				this.mainDiv = document.createElement("div");',
  '				this.mainDiv.className = "separator";',
  '				this.mainDiv.innerHTML = "&nbsp;";',
  '			}',
  '',
  '			SeparatorElementContainer.prototype = new LogItemContainerElement();',
  '',
  '			SeparatorElementContainer.prototype.remove = function() {',
  '				this.mainDiv.parentNode.removeChild(this.mainDiv);',
  '				this.mainDiv = null;',
  '			};',
  '',
  '			/*----------------------------------------------------------------*/',
  '',
  '			function Separator() {',
  '				this.rendered = false;',
  '			}',
  '',
  '			Separator.prototype = new LogItem();',
  '',
  '			copyProperties(Separator.prototype, {',
  '				render: function() {',
  '					var containerDomNode = this.group.contentDiv;',
  '					if (isIe) {',
  '						this.unwrappedElementContainer = new SeparatorElementContainer(this.getUnwrappedDomContainer());',
  '						this.wrappedElementContainer = new SeparatorElementContainer(this.getWrappedDomContainer());',
  '						this.elementContainers = [this.unwrappedElementContainer, this.wrappedElementContainer];',
  '					} else {',
  '						this.mainElementContainer = new SeparatorElementContainer(this.getMainDomContainer());',
  '						this.elementContainers = [this.mainElementContainer];',
  '					}',
  '					this.content = this.formattedMessage;',
  '					this.rendered = true;',
  '				}',
  '			});',
  '',
  '			/*----------------------------------------------------------------*/',
  '',
  '			function GroupElementContainer(group, containerDomNode, isRoot, isWrapped) {',
  '				this.group = group;',
  '				this.containerDomNode = containerDomNode;',
  '				this.isRoot = isRoot;',
  '				this.isWrapped = isWrapped;',
  '				this.expandable = false;',
  '',
  '				if (this.isRoot) {',
  '					if (isIe) {',
  '						this.contentDiv = logMainContainer.appendChild(document.createElement("div"));',
  '						this.contentDiv.id = this.isWrapped ? "log_wrapped" : "log_unwrapped";',
  '					} else {',
  '						this.contentDiv = logMainContainer;',
  '					}',
  '				} else {',
  '					var groupElementContainer = this;',
  '					',
  '					this.mainDiv = document.createElement("div");',
  '					this.mainDiv.className = "group";',
  '',
  '					this.headingDiv = this.mainDiv.appendChild(document.createElement("div"));',
  '					this.headingDiv.className = "groupheading";',
  '',
  '					this.expander = this.headingDiv.appendChild(document.createElement("span"));',
  '					this.expander.className = "expander unselectable greyedout";',
  '					this.expander.unselectable = true;',
  '					var expanderText = this.group.expanded ? "-" : "+";',
  '					this.expanderTextNode = this.expander.appendChild(document.createTextNode(expanderText));',
  '					',
  '					this.headingDiv.appendChild(document.createTextNode(" " + this.group.name));',
  '',
  '					this.contentDiv = this.mainDiv.appendChild(document.createElement("div"));',
  '					var contentCssClass = this.group.expanded ? "expanded" : "collapsed";',
  '					this.contentDiv.className = "groupcontent " + contentCssClass;',
  '',
  '					this.expander.onclick = function() {',
  '						if (groupElementContainer.group.expandable) {',
  '							groupElementContainer.group.toggleExpanded();',
  '						}',
  '					};',
  '				}',
  '			}',
  '',
  '			GroupElementContainer.prototype = new LogItemContainerElement();',
  '',
  '			copyProperties(GroupElementContainer.prototype, {',
  '				toggleExpanded: function() {',
  '					if (!this.isRoot) {',
  '						var oldCssClass, newCssClass, expanderText;',
  '						if (this.group.expanded) {',
  '							newCssClass = "expanded";',
  '							oldCssClass = "collapsed";',
  '							expanderText = "-";',
  '						} else {',
  '							newCssClass = "collapsed";',
  '							oldCssClass = "expanded";',
  '							expanderText = "+";',
  '						}',
  '						replaceClass(this.contentDiv, newCssClass, oldCssClass);',
  '						this.expanderTextNode.nodeValue = expanderText;',
  '					}',
  '				},',
  '',
  '				remove: function() {',
  '					if (!this.isRoot) {',
  '						this.headingDiv = null;',
  '						this.expander.onclick = null;',
  '						this.expander = null;',
  '						this.expanderTextNode = null;',
  '						this.contentDiv = null;',
  '						this.containerDomNode = null;',
  '						this.mainDiv.parentNode.removeChild(this.mainDiv);',
  '						this.mainDiv = null;',
  '					}',
  '				},',
  '',
  '				reverseChildren: function() {',
  '					// Invert the order of the log entries',
  '					var node = null;',
  '',
  '					// Remove all the log container nodes',
  '					var childDomNodes = [];',
  '					while ((node = this.contentDiv.firstChild)) {',
  '						this.contentDiv.removeChild(node);',
  '						childDomNodes.push(node);',
  '					}',
  '',
  '					// Put them all back in reverse order',
  '					while ((node = childDomNodes.pop())) {',
  '						this.contentDiv.appendChild(node);',
  '					}',
  '				},',
  '',
  '				update: function() {',
  '					if (!this.isRoot) {',
  '						if (this.group.expandable) {',
  '							removeClass(this.expander, "greyedout");',
  '						} else {',
  '							addClass(this.expander, "greyedout");',
  '						}',
  '					}',
  '				},',
  '',
  '				clear: function() {',
  '					if (this.isRoot) {',
  '						this.contentDiv.innerHTML = "";',
  '					}',
  '				}',
  '			});',
  '',
  '			/*----------------------------------------------------------------*/',
  '',
  '			function Group(name, isRoot, initiallyExpanded) {',
  '				this.name = name;',
  '				this.group = null;',
  '				this.isRoot = isRoot;',
  '				this.initiallyExpanded = initiallyExpanded;',
  '				this.elementContainers = [];',
  '				this.children = [];',
  '				this.expanded = initiallyExpanded;',
  '				this.rendered = false;',
  '				this.expandable = false;',
  '			}',
  '',
  '			Group.prototype = new LogItem();',
  '',
  '			copyProperties(Group.prototype, {',
  '				addChild: function(logItem) {',
  '					this.children.push(logItem);',
  '					logItem.group = this;',
  '				},',
  '',
  '				render: function() {',
  '					if (isIe) {',
  '						var unwrappedDomContainer, wrappedDomContainer;',
  '						if (this.isRoot) {',
  '							unwrappedDomContainer = logMainContainer;',
  '							wrappedDomContainer = logMainContainer;',
  '						} else {',
  '							unwrappedDomContainer = this.getUnwrappedDomContainer();',
  '							wrappedDomContainer = this.getWrappedDomContainer();',
  '						}',
  '						this.unwrappedElementContainer = new GroupElementContainer(this, unwrappedDomContainer, this.isRoot, false);',
  '						this.wrappedElementContainer = new GroupElementContainer(this, wrappedDomContainer, this.isRoot, true);',
  '						this.elementContainers = [this.unwrappedElementContainer, this.wrappedElementContainer];',
  '					} else {',
  '						var mainDomContainer = this.isRoot ? logMainContainer : this.getMainDomContainer();',
  '						this.mainElementContainer = new GroupElementContainer(this, mainDomContainer, this.isRoot, false);',
  '						this.elementContainers = [this.mainElementContainer];',
  '					}',
  '					this.rendered = true;',
  '				},',
  '',
  '				toggleExpanded: function() {',
  '					this.expanded = !this.expanded;',
  '					for (var i = 0, len = this.elementContainers.length; i < len; i++) {',
  '						this.elementContainers[i].toggleExpanded();',
  '					}',
  '				},',
  '',
  '				expand: function() {',
  '					if (!this.expanded) {',
  '						this.toggleExpanded();',
  '					}',
  '				},',
  '',
  '				accept: function(visitor) {',
  '					visitor.visitGroup(this);',
  '				},',
  '',
  '				reverseChildren: function() {',
  '					if (this.rendered) {',
  '						for (var i = 0, len = this.elementContainers.length; i < len; i++) {',
  '							this.elementContainers[i].reverseChildren();',
  '						}',
  '					}',
  '				},',
  '',
  '				update: function() {',
  '					var previouslyExpandable = this.expandable;',
  '					this.expandable = (this.children.length !== 0);',
  '					if (this.expandable !== previouslyExpandable) {',
  '						for (var i = 0, len = this.elementContainers.length; i < len; i++) {',
  '							this.elementContainers[i].update();',
  '						}',
  '					}',
  '				},',
  '',
  '				flatten: function() {',
  '					var visitor = new GroupFlattener();',
  '					this.accept(visitor);',
  '					return visitor.logEntriesAndSeparators;',
  '				},',
  '',
  '				removeChild: function(child, doUpdate) {',
  '					array_remove(this.children, child);',
  '					child.group = null;',
  '					if (doUpdate) {',
  '						this.update();',
  '					}',
  '				},',
  '',
  '				remove: function(doUpdate, removeFromGroup) {',
  '					for (var i = 0, len = this.children.length; i < len; i++) {',
  '						this.children[i].remove(false, false);',
  '					}',
  '					this.children = [];',
  '					this.update();',
  '					if (this === currentGroup) {',
  '						currentGroup = this.group;',
  '					}',
  '					this.doRemove(doUpdate, removeFromGroup);',
  '				},',
  '',
  '				serialize: function(items) {',
  '					items.push([LogItem.serializedItemKeys.GROUP_START, this.name]);',
  '					for (var i = 0, len = this.children.length; i < len; i++) {',
  '						this.children[i].serialize(items);',
  '					}',
  '					if (this !== currentGroup) {',
  '						items.push([LogItem.serializedItemKeys.GROUP_END]);',
  '					}',
  '				},',
  '',
  '				clear: function() {',
  '					for (var i = 0, len = this.elementContainers.length; i < len; i++) {',
  '						this.elementContainers[i].clear();',
  '					}',
  '				}',
  '			});',
  '',
  '			/*----------------------------------------------------------------*/',
  '',
  '			function LogEntryElementContainer() {',
  '			}',
  '',
  '			LogEntryElementContainer.prototype = new LogItemContainerElement();',
  '',
  '			copyProperties(LogEntryElementContainer.prototype, {',
  '				remove: function() {',
  '					this.doRemove();',
  '				},',
  '',
  '				doRemove: function() {',
  '					this.mainDiv.parentNode.removeChild(this.mainDiv);',
  '					this.mainDiv = null;',
  '					this.contentElement = null;',
  '					this.containerDomNode = null;',
  '				},',
  '',
  '				setContent: function(content, wrappedContent) {',
  '					if (content === this.formattedMessage) {',
  '						this.contentElement.innerHTML = "";',
  '						this.contentElement.appendChild(document.createTextNode(this.formattedMessage));',
  '					} else {',
  '						this.contentElement.innerHTML = content;',
  '					}',
  '				},',
  '',
  '				setSearchMatch: function(isMatch) {',
  '					var oldCssClass = isMatch ? "searchnonmatch" : "searchmatch";',
  '					var newCssClass = isMatch ? "searchmatch" : "searchnonmatch";',
  '					replaceClass(this.mainDiv, newCssClass, oldCssClass);',
  '				},',
  '',
  '				clearSearch: function() {',
  '					removeClass(this.mainDiv, "searchmatch");',
  '					removeClass(this.mainDiv, "searchnonmatch");',
  '				}',
  '			});',
  '',
  '			/*----------------------------------------------------------------*/',
  '',
  '			function LogEntryWrappedElementContainer(logEntry, containerDomNode) {',
  '				this.logEntry = logEntry;',
  '				this.containerDomNode = containerDomNode;',
  '				this.mainDiv = document.createElement("div");',
  '				this.mainDiv.appendChild(document.createTextNode(this.logEntry.formattedMessage));',
  '				this.mainDiv.className = "logentry wrapped " + this.logEntry.level;',
  '				this.contentElement = this.mainDiv;',
  '			}',
  '',
  '			LogEntryWrappedElementContainer.prototype = new LogEntryElementContainer();',
  '',
  '			LogEntryWrappedElementContainer.prototype.setContent = function(content, wrappedContent) {',
  '				if (content === this.formattedMessage) {',
  '					this.contentElement.innerHTML = "";',
  '					this.contentElement.appendChild(document.createTextNode(this.formattedMessage));',
  '				} else {',
  '					this.contentElement.innerHTML = wrappedContent;',
  '				}',
  '			};',
  '',
  '			/*----------------------------------------------------------------*/',
  '',
  '			function LogEntryUnwrappedElementContainer(logEntry, containerDomNode) {',
  '				this.logEntry = logEntry;',
  '				this.containerDomNode = containerDomNode;',
  '				this.mainDiv = document.createElement("div");',
  '				this.mainDiv.className = "logentry unwrapped " + this.logEntry.level;',
  '				this.pre = this.mainDiv.appendChild(document.createElement("pre"));',
  '				this.pre.appendChild(document.createTextNode(this.logEntry.formattedMessage));',
  '				this.pre.className = "unwrapped";',
  '				this.contentElement = this.pre;',
  '			}',
  '',
  '			LogEntryUnwrappedElementContainer.prototype = new LogEntryElementContainer();',
  '',
  '			LogEntryUnwrappedElementContainer.prototype.remove = function() {',
  '				this.doRemove();',
  '				this.pre = null;',
  '			};',
  '',
  '			/*----------------------------------------------------------------*/',
  '',
  '			function LogEntryMainElementContainer(logEntry, containerDomNode) {',
  '				this.logEntry = logEntry;',
  '				this.containerDomNode = containerDomNode;',
  '				this.mainDiv = document.createElement("div");',
  '				this.mainDiv.className = "logentry nonielogentry " + this.logEntry.level;',
  '				this.contentElement = this.mainDiv.appendChild(document.createElement("span"));',
  '				this.contentElement.appendChild(document.createTextNode(this.logEntry.formattedMessage));',
  '			}',
  '',
  '			LogEntryMainElementContainer.prototype = new LogEntryElementContainer();',
  '',
  '			/*----------------------------------------------------------------*/',
  '',
  '			function LogEntry(level, formattedMessage) {',
  '				this.level = level;',
  '				this.formattedMessage = formattedMessage;',
  '				this.rendered = false;',
  '			}',
  '',
  '			LogEntry.prototype = new LogItem();',
  '',
  '			copyProperties(LogEntry.prototype, {',
  '				render: function() {',
  '					var logEntry = this;',
  '					var containerDomNode = this.group.contentDiv;',
  '',
  '					// Support for the CSS attribute white-space in IE for Windows is',
  '					// non-existent pre version 6 and slightly odd in 6, so instead',
  '					// use two different HTML elements',
  '					if (isIe) {',
  '						this.formattedMessage = this.formattedMessage.replace(/\\r\\n/g, "\\r"); // Workaround for IE\'s treatment of white space',
  '						this.unwrappedElementContainer = new LogEntryUnwrappedElementContainer(this, this.getUnwrappedDomContainer());',
  '						this.wrappedElementContainer = new LogEntryWrappedElementContainer(this, this.getWrappedDomContainer());',
  '						this.elementContainers = [this.unwrappedElementContainer, this.wrappedElementContainer];',
  '					} else {',
  '						this.mainElementContainer = new LogEntryMainElementContainer(this, this.getMainDomContainer());',
  '						this.elementContainers = [this.mainElementContainer];',
  '					}',
  '					this.content = this.formattedMessage;',
  '					this.rendered = true;',
  '				},',
  '',
  '				setContent: function(content, wrappedContent) {',
  '					if (content != this.content) {',
  '						if (isIe && (content !== this.formattedMessage)) {',
  '							content = content.replace(/\\r\\n/g, "\\r"); // Workaround for IE\'s treatment of white space',
  '						}',
  '						for (var i = 0, len = this.elementContainers.length; i < len; i++) {',
  '							this.elementContainers[i].setContent(content, wrappedContent);',
  '						}',
  '						this.content = content;',
  '					}',
  '				},',
  '',
  '				getSearchMatches: function() {',
  '					var matches = [];',
  '					var i, len;',
  '					if (isIe) {',
  '						var unwrappedEls = getElementsByClass(this.unwrappedElementContainer.mainDiv, "searchterm", "span");',
  '						var wrappedEls = getElementsByClass(this.wrappedElementContainer.mainDiv, "searchterm", "span");',
  '						for (i = 0, len = unwrappedEls.length; i < len; i++) {',
  '							matches[i] = new Match(this.level, null, unwrappedEls[i], wrappedEls[i]);',
  '						}',
  '					} else {',
  '						var els = getElementsByClass(this.mainElementContainer.mainDiv, "searchterm", "span");',
  '						for (i = 0, len = els.length; i < len; i++) {',
  '							matches[i] = new Match(this.level, els[i]);',
  '						}',
  '					}',
  '					return matches;',
  '				},',
  '',
  '				setSearchMatch: function(isMatch) {',
  '					for (var i = 0, len = this.elementContainers.length; i < len; i++) {',
  '						this.elementContainers[i].setSearchMatch(isMatch);',
  '					}',
  '				},',
  '',
  '				clearSearch: function() {',
  '					for (var i = 0, len = this.elementContainers.length; i < len; i++) {',
  '						this.elementContainers[i].clearSearch();',
  '					}',
  '				},',
  '',
  '				accept: function(visitor) {',
  '					visitor.visitLogEntry(this);',
  '				},',
  '',
  '				serialize: function(items) {',
  '					items.push([LogItem.serializedItemKeys.LOG_ENTRY, this.level, this.formattedMessage]);',
  '				}',
  '			});',
  '',
  '			/*----------------------------------------------------------------*/',
  '',
  '			function LogItemVisitor() {',
  '			}',
  '',
  '			LogItemVisitor.prototype = {',
  '				visit: function(logItem) {',
  '				},',
  '',
  '				visitParent: function(logItem) {',
  '					if (logItem.group) {',
  '						logItem.group.accept(this);',
  '					}',
  '				},',
  '',
  '				visitChildren: function(logItem) {',
  '					for (var i = 0, len = logItem.children.length; i < len; i++) {',
  '						logItem.children[i].accept(this);',
  '					}',
  '				},',
  '',
  '				visitLogEntry: function(logEntry) {',
  '					this.visit(logEntry);',
  '				},',
  '',
  '				visitSeparator: function(separator) {',
  '					this.visit(separator);',
  '				},',
  '',
  '				visitGroup: function(group) {',
  '					this.visit(group);',
  '				}',
  '			};',
  '',
  '			/*----------------------------------------------------------------*/',
  '',
  '			function GroupFlattener() {',
  '				this.logEntriesAndSeparators = [];',
  '			}',
  '',
  '			GroupFlattener.prototype = new LogItemVisitor();',
  '',
  '			GroupFlattener.prototype.visitGroup = function(group) {',
  '				this.visitChildren(group);',
  '			};',
  '',
  '			GroupFlattener.prototype.visitLogEntry = function(logEntry) {',
  '				this.logEntriesAndSeparators.push(logEntry);',
  '			};',
  '',
  '			GroupFlattener.prototype.visitSeparator = function(separator) {',
  '				this.logEntriesAndSeparators.push(separator);',
  '			};',
  '',
  '			/*----------------------------------------------------------------*/',
  '',
  '			window.onload = function() {',
  '				// Sort out document.domain',
  '				if (location.search) {',
  '					var queryBits = unescape(location.search).substr(1).split("&"), nameValueBits;',
  '					for (var i = 0, len = queryBits.length; i < len; i++) {',
  '						nameValueBits = queryBits[i].split("=");',
  '						if (nameValueBits[0] == "log4javascript_domain") {',
  '							document.domain = nameValueBits[1];',
  '							break;',
  '						}',
  '					}',
  '				}',
  '',
  '				// Create DOM objects',
  '				logMainContainer = $("log");',
  '				if (isIePre7) {',
  '					addClass(logMainContainer, "oldIe");',
  '				}',
  '',
  '				rootGroup = new Group("root", true);',
  '				rootGroup.render();',
  '				currentGroup = rootGroup;',
  '				',
  '				setCommandInputWidth();',
  '				setLogContainerHeight();',
  '				toggleLoggingEnabled();',
  '				toggleSearchEnabled();',
  '				toggleSearchFilter();',
  '				toggleSearchHighlight();',
  '				applyFilters();',
  '				checkAllLevels();',
  '				toggleWrap();',
  '				toggleNewestAtTop();',
  '				toggleScrollToLatest();',
  '				renderQueuedLogItems();',
  '				loaded = true;',
  '				$("command").value = "";',
  '				$("command").autocomplete = "off";',
  '				$("command").onkeydown = function(evt) {',
  '					evt = getEvent(evt);',
  '					if (evt.keyCode == 10 || evt.keyCode == 13) { // Return/Enter',
  '						evalCommandLine();',
  '						stopPropagation(evt);',
  '					} else if (evt.keyCode == 27) { // Escape',
  '						this.value = "";',
  '						this.focus();',
  '					} else if (evt.keyCode == 38 && commandHistory.length > 0) { // Up',
  '						currentCommandIndex = Math.max(0, currentCommandIndex - 1);',
  '						this.value = commandHistory[currentCommandIndex];',
  '						moveCaretToEnd(this);',
  '					} else if (evt.keyCode == 40 && commandHistory.length > 0) { // Down',
  '						currentCommandIndex = Math.min(commandHistory.length - 1, currentCommandIndex + 1);',
  '						this.value = commandHistory[currentCommandIndex];',
  '						moveCaretToEnd(this);',
  '					}',
  '				};',
  '',
  '				// Prevent the keypress moving the caret in Firefox',
  '				$("command").onkeypress = function(evt) {',
  '					evt = getEvent(evt);',
  '					if (evt.keyCode == 38 && commandHistory.length > 0 && evt.preventDefault) { // Up',
  '						evt.preventDefault();',
  '					}',
  '				};',
  '',
  '				// Prevent the keyup event blurring the input in Opera',
  '				$("command").onkeyup = function(evt) {',
  '					evt = getEvent(evt);',
  '					if (evt.keyCode == 27 && evt.preventDefault) { // Up',
  '						evt.preventDefault();',
  '						this.focus();',
  '					}',
  '				};',
  '',
  '				// Add document keyboard shortcuts',
  '				document.onkeydown = function keyEventHandler(evt) {',
  '					evt = getEvent(evt);',
  '					switch (evt.keyCode) {',
  '						case 69: // Ctrl + shift + E: re-execute last command',
  '							if (evt.shiftKey && (evt.ctrlKey || evt.metaKey)) {',
  '								evalLastCommand();',
  '								cancelKeyEvent(evt);',
  '								return false;',
  '							}',
  '							break;',
  '						case 75: // Ctrl + shift + K: focus search',
  '							if (evt.shiftKey && (evt.ctrlKey || evt.metaKey)) {',
  '								focusSearch();',
  '								cancelKeyEvent(evt);',
  '								return false;',
  '							}',
  '							break;',
  '						case 40: // Ctrl + shift + down arrow: focus command line',
  '						case 76: // Ctrl + shift + L: focus command line',
  '							if (evt.shiftKey && (evt.ctrlKey || evt.metaKey)) {',
  '								focusCommandLine();',
  '								cancelKeyEvent(evt);',
  '								return false;',
  '							}',
  '							break;',
  '					}',
  '				};',
  '',
  '				// Workaround to make sure log div starts at the correct size',
  '				setTimeout(setLogContainerHeight, 20);',
  '',
  '				setShowCommandLine(showCommandLine);',
  '				doSearch();',
  '			};',
  '',
  '			window.onunload = function() {',
  '				if (mainWindowExists()) {',
  '					appender.unload();',
  '				}',
  '				appender = null;',
  '			};',
  '',
  '			/*----------------------------------------------------------------*/',
  '',
  '			function toggleLoggingEnabled() {',
  '				setLoggingEnabled($("enableLogging").checked);',
  '			}',
  '',
  '			function setLoggingEnabled(enable) {',
  '				loggingEnabled = enable;',
  '			}',
  '',
  '			var appender = null;',
  '',
  '			function setAppender(appenderParam) {',
  '				appender = appenderParam;',
  '			}',
  '',
  '			function setShowCloseButton(showCloseButton) {',
  '				$("closeButton").style.display = showCloseButton ? "inline" : "none";',
  '			}',
  '',
  '			function setShowHideButton(showHideButton) {',
  '				$("hideButton").style.display = showHideButton ? "inline" : "none";',
  '			}',
  '',
  '			var newestAtTop = false;',
  '',
  '			/*----------------------------------------------------------------*/',
  '',
  '			function LogItemContentReverser() {',
  '			}',
  '			',
  '			LogItemContentReverser.prototype = new LogItemVisitor();',
  '			',
  '			LogItemContentReverser.prototype.visitGroup = function(group) {',
  '				group.reverseChildren();',
  '				this.visitChildren(group);',
  '			};',
  '',
  '			/*----------------------------------------------------------------*/',
  '',
  '			function setNewestAtTop(isNewestAtTop) {',
  '				var oldNewestAtTop = newestAtTop;',
  '				var i, iLen, j, jLen;',
  '				newestAtTop = Boolean(isNewestAtTop);',
  '				if (oldNewestAtTop != newestAtTop) {',
  '					var visitor = new LogItemContentReverser();',
  '					rootGroup.accept(visitor);',
  '',
  '					// Reassemble the matches array',
  '					if (currentSearch) {',
  '						var currentMatch = currentSearch.matches[currentMatchIndex];',
  '						var matchIndex = 0;',
  '						var matches = [];',
  '						var actOnLogEntry = function(logEntry) {',
  '							var logEntryMatches = logEntry.getSearchMatches();',
  '							for (j = 0, jLen = logEntryMatches.length; j < jLen; j++) {',
  '								matches[matchIndex] = logEntryMatches[j];',
  '								if (currentMatch && logEntryMatches[j].equals(currentMatch)) {',
  '									currentMatchIndex = matchIndex;',
  '								}',
  '								matchIndex++;',
  '							}',
  '						};',
  '						if (newestAtTop) {',
  '							for (i = logEntries.length - 1; i >= 0; i--) {',
  '								actOnLogEntry(logEntries[i]);',
  '							}',
  '						} else {',
  '							for (i = 0, iLen = logEntries.length; i < iLen; i++) {',
  '								actOnLogEntry(logEntries[i]);',
  '							}',
  '						}',
  '						currentSearch.matches = matches;',
  '						if (currentMatch) {',
  '							currentMatch.setCurrent();',
  '						}',
  '					} else if (scrollToLatest) {',
  '						doScrollToLatest();',
  '					}',
  '				}',
  '				$("newestAtTop").checked = isNewestAtTop;',
  '			}',
  '',
  '			function toggleNewestAtTop() {',
  '				var isNewestAtTop = $("newestAtTop").checked;',
  '				setNewestAtTop(isNewestAtTop);',
  '			}',
  '',
  '			var scrollToLatest = true;',
  '',
  '			function setScrollToLatest(isScrollToLatest) {',
  '				scrollToLatest = isScrollToLatest;',
  '				if (scrollToLatest) {',
  '					doScrollToLatest();',
  '				}',
  '				$("scrollToLatest").checked = isScrollToLatest;',
  '			}',
  '',
  '			function toggleScrollToLatest() {',
  '				var isScrollToLatest = $("scrollToLatest").checked;',
  '				setScrollToLatest(isScrollToLatest);',
  '			}',
  '',
  '			function doScrollToLatest() {',
  '				var l = logMainContainer;',
  '				if (typeof l.scrollTop != "undefined") {',
  '					if (newestAtTop) {',
  '						l.scrollTop = 0;',
  '					} else {',
  '						var latestLogEntry = l.lastChild;',
  '						if (latestLogEntry) {',
  '							l.scrollTop = l.scrollHeight;',
  '						}',
  '					}',
  '				}',
  '			}',
  '',
  '			var closeIfOpenerCloses = true;',
  '',
  '			function setCloseIfOpenerCloses(isCloseIfOpenerCloses) {',
  '				closeIfOpenerCloses = isCloseIfOpenerCloses;',
  '			}',
  '',
  '			var maxMessages = null;',
  '',
  '			function setMaxMessages(max) {',
  '				maxMessages = max;',
  '				pruneLogEntries();',
  '			}',
  '',
  '			var showCommandLine = false;',
  '',
  '			function setShowCommandLine(isShowCommandLine) {',
  '				showCommandLine = isShowCommandLine;',
  '				if (loaded) {',
  '					$("commandLine").style.display = showCommandLine ? "block" : "none";',
  '					setCommandInputWidth();',
  '					setLogContainerHeight();',
  '				}',
  '			}',
  '',
  '			function focusCommandLine() {',
  '				if (loaded) {',
  '					$("command").focus();',
  '				}',
  '			}',
  '',
  '			function focusSearch() {',
  '				if (loaded) {',
  '					$("searchBox").focus();',
  '				}',
  '			}',
  '',
  '			function getLogItems() {',
  '				var items = [];',
  '				for (var i = 0, len = logItems.length; i < len; i++) {',
  '					logItems[i].serialize(items);',
  '				}',
  '				return items;',
  '			}',
  '',
  '			function setLogItems(items) {',
  '				var loggingReallyEnabled = loggingEnabled;',
  '				// Temporarily turn logging on',
  '				loggingEnabled = true;',
  '				for (var i = 0, len = items.length; i < len; i++) {',
  '					switch (items[i][0]) {',
  '						case LogItem.serializedItemKeys.LOG_ENTRY:',
  '							log(items[i][1], items[i][2]);',
  '							break;',
  '						case LogItem.serializedItemKeys.GROUP_START:',
  '							group(items[i][1]);',
  '							break;',
  '						case LogItem.serializedItemKeys.GROUP_END:',
  '							groupEnd();',
  '							break;',
  '					}',
  '				}',
  '				loggingEnabled = loggingReallyEnabled;',
  '			}',
  '',
  '			function log(logLevel, formattedMessage) {',
  '				if (loggingEnabled) {',
  '					var logEntry = new LogEntry(logLevel, formattedMessage);',
  '					logEntries.push(logEntry);',
  '					logEntriesAndSeparators.push(logEntry);',
  '					logItems.push(logEntry);',
  '					currentGroup.addChild(logEntry);',
  '					if (loaded) {',
  '						if (logQueuedEventsTimer !== null) {',
  '							clearTimeout(logQueuedEventsTimer);',
  '						}',
  '						logQueuedEventsTimer = setTimeout(renderQueuedLogItems, renderDelay);',
  '						unrenderedLogItemsExist = true;',
  '					}',
  '				}',
  '			}',
  '',
  '			function renderQueuedLogItems() {',
  '				logQueuedEventsTimer = null;',
  '				var pruned = pruneLogEntries();',
  '',
  '				// Render any unrendered log entries and apply the current search to them',
  '				var initiallyHasMatches = currentSearch ? currentSearch.hasMatches() : false;',
  '				for (var i = 0, len = logItems.length; i < len; i++) {',
  '					if (!logItems[i].rendered) {',
  '						logItems[i].render();',
  '						logItems[i].appendToLog();',
  '						if (currentSearch && (logItems[i] instanceof LogEntry)) {',
  '							currentSearch.applyTo(logItems[i]);',
  '						}',
  '					}',
  '				}',
  '				if (currentSearch) {',
  '					if (pruned) {',
  '						if (currentSearch.hasVisibleMatches()) {',
  '							if (currentMatchIndex === null) {',
  '								setCurrentMatchIndex(0);',
  '							}',
  '							displayMatches();',
  '						} else {',
  '							displayNoMatches();',
  '						}',
  '					} else if (!initiallyHasMatches && currentSearch.hasVisibleMatches()) {',
  '						setCurrentMatchIndex(0);',
  '						displayMatches();',
  '					}',
  '				}',
  '				if (scrollToLatest) {',
  '					doScrollToLatest();',
  '				}',
  '				unrenderedLogItemsExist = false;',
  '			}',
  '',
  '			function pruneLogEntries() {',
  '				if ((maxMessages !== null) && (logEntriesAndSeparators.length > maxMessages)) {',
  '					var numberToDelete = logEntriesAndSeparators.length - maxMessages;',
  '					var prunedLogEntries = logEntriesAndSeparators.slice(0, numberToDelete);',
  '					if (currentSearch) {',
  '						currentSearch.removeMatches(prunedLogEntries);',
  '					}',
  '					var group;',
  '					for (var i = 0; i < numberToDelete; i++) {',
  '						group = logEntriesAndSeparators[i].group;',
  '						array_remove(logItems, logEntriesAndSeparators[i]);',
  '						array_remove(logEntries, logEntriesAndSeparators[i]);',
  '						logEntriesAndSeparators[i].remove(true, true);',
  '						if (group.children.length === 0 && group !== currentGroup && group !== rootGroup) {',
  '							array_remove(logItems, group);',
  '							group.remove(true, true);',
  '						}',
  '					}',
  '					logEntriesAndSeparators = array_removeFromStart(logEntriesAndSeparators, numberToDelete);',
  '					return true;',
  '				}',
  '				return false;',
  '			}',
  '',
  '			function group(name, startExpanded) {',
  '				if (loggingEnabled) {',
  '					initiallyExpanded = (typeof startExpanded === "undefined") ? true : Boolean(startExpanded);',
  '					var newGroup = new Group(name, false, initiallyExpanded);',
  '					currentGroup.addChild(newGroup);',
  '					currentGroup = newGroup;',
  '					logItems.push(newGroup);',
  '					if (loaded) {',
  '						if (logQueuedEventsTimer !== null) {',
  '							clearTimeout(logQueuedEventsTimer);',
  '						}',
  '						logQueuedEventsTimer = setTimeout(renderQueuedLogItems, renderDelay);',
  '						unrenderedLogItemsExist = true;',
  '					}',
  '				}',
  '			}',
  '',
  '			function groupEnd() {',
  '				currentGroup = (currentGroup === rootGroup) ? rootGroup : currentGroup.group;',
  '			}',
  '',
  '			function mainPageReloaded() {',
  '				currentGroup = rootGroup;',
  '				var separator = new Separator();',
  '				logEntriesAndSeparators.push(separator);',
  '				logItems.push(separator);',
  '				currentGroup.addChild(separator);',
  '			}',
  '',
  '			function closeWindow() {',
  '				if (appender && mainWindowExists()) {',
  '					appender.close(true);',
  '				} else {',
  '					window.close();',
  '				}',
  '			}',
  '',
  '			function hide() {',
  '				if (appender && mainWindowExists()) {',
  '					appender.hide();',
  '				}',
  '			}',
  '',
  '			var mainWindow = window;',
  '			var windowId = "log4javascriptConsoleWindow_" + new Date().getTime() + "_" + ("" + Math.random()).substr(2);',
  '',
  '			function setMainWindow(win) {',
  '				mainWindow = win;',
  '				mainWindow[windowId] = window;',
  '				// If this is a pop-up, poll the opener to see if it\'s closed',
  '				if (opener && closeIfOpenerCloses) {',
  '					pollOpener();',
  '				}',
  '			}',
  '',
  '			function pollOpener() {',
  '				if (closeIfOpenerCloses) {',
  '					if (mainWindowExists()) {',
  '						setTimeout(pollOpener, 500);',
  '					} else {',
  '						closeWindow();',
  '					}',
  '				}',
  '			}',
  '',
  '			function mainWindowExists() {',
  '				try {',
  '					return (mainWindow && !mainWindow.closed &&',
  '						mainWindow[windowId] == window);',
  '				} catch (ex) {}',
  '				return false;',
  '			}',
  '',
  '			var logLevels = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"];',
  '',
  '			function getCheckBox(logLevel) {',
  '				return $("switch_" + logLevel);',
  '			}',
  '',
  '			function getIeWrappedLogContainer() {',
  '				return $("log_wrapped");',
  '			}',
  '',
  '			function getIeUnwrappedLogContainer() {',
  '				return $("log_unwrapped");',
  '			}',
  '',
  '			function applyFilters() {',
  '				for (var i = 0; i < logLevels.length; i++) {',
  '					if (getCheckBox(logLevels[i]).checked) {',
  '						addClass(logMainContainer, logLevels[i]);',
  '					} else {',
  '						removeClass(logMainContainer, logLevels[i]);',
  '					}',
  '				}',
  '				updateSearchFromFilters();',
  '			}',
  '',
  '			function toggleAllLevels() {',
  '				var turnOn = $("switch_ALL").checked;',
  '				for (var i = 0; i < logLevels.length; i++) {',
  '					getCheckBox(logLevels[i]).checked = turnOn;',
  '					if (turnOn) {',
  '						addClass(logMainContainer, logLevels[i]);',
  '					} else {',
  '						removeClass(logMainContainer, logLevels[i]);',
  '					}',
  '				}',
  '			}',
  '',
  '			function checkAllLevels() {',
  '				for (var i = 0; i < logLevels.length; i++) {',
  '					if (!getCheckBox(logLevels[i]).checked) {',
  '						getCheckBox("ALL").checked = false;',
  '						return;',
  '					}',
  '				}',
  '				getCheckBox("ALL").checked = true;',
  '			}',
  '',
  '			function clearLog() {',
  '				rootGroup.clear();',
  '				currentGroup = rootGroup;',
  '				logEntries = [];',
  '				logItems = [];',
  '				logEntriesAndSeparators = [];',
  ' 				doSearch();',
  '			}',
  '',
  '			function toggleWrap() {',
  '				var enable = $("wrap").checked;',
  '				if (enable) {',
  '					addClass(logMainContainer, "wrap");',
  '				} else {',
  '					removeClass(logMainContainer, "wrap");',
  '				}',
  '				refreshCurrentMatch();',
  '			}',
  '',
  '			/* ------------------------------------------------------------------- */',
  '',
  '			// Search',
  '',
  '			var searchTimer = null;',
  '',
  '			function scheduleSearch() {',
  '				try {',
  '					clearTimeout(searchTimer);',
  '				} catch (ex) {',
  '					// Do nothing',
  '				}',
  '				searchTimer = setTimeout(doSearch, 500);',
  '			}',
  '',
  '			function Search(searchTerm, isRegex, searchRegex, isCaseSensitive) {',
  '				this.searchTerm = searchTerm;',
  '				this.isRegex = isRegex;',
  '				this.searchRegex = searchRegex;',
  '				this.isCaseSensitive = isCaseSensitive;',
  '				this.matches = [];',
  '			}',
  '',
  '			Search.prototype = {',
  '				hasMatches: function() {',
  '					return this.matches.length > 0;',
  '				},',
  '',
  '				hasVisibleMatches: function() {',
  '					if (this.hasMatches()) {',
  '						for (var i = 0; i < this.matches.length; i++) {',
  '							if (this.matches[i].isVisible()) {',
  '								return true;',
  '							}',
  '						}',
  '					}',
  '					return false;',
  '				},',
  '',
  '				match: function(logEntry) {',
  '					var entryText = String(logEntry.formattedMessage);',
  '					var matchesSearch = false;',
  '					if (this.isRegex) {',
  '						matchesSearch = this.searchRegex.test(entryText);',
  '					} else if (this.isCaseSensitive) {',
  '						matchesSearch = (entryText.indexOf(this.searchTerm) > -1);',
  '					} else {',
  '						matchesSearch = (entryText.toLowerCase().indexOf(this.searchTerm.toLowerCase()) > -1);',
  '					}',
  '					return matchesSearch;',
  '				},',
  '',
  '				getNextVisibleMatchIndex: function() {',
  '					for (var i = currentMatchIndex + 1; i < this.matches.length; i++) {',
  '						if (this.matches[i].isVisible()) {',
  '							return i;',
  '						}',
  '					}',
  '					// Start again from the first match',
  '					for (i = 0; i <= currentMatchIndex; i++) {',
  '						if (this.matches[i].isVisible()) {',
  '							return i;',
  '						}',
  '					}',
  '					return -1;',
  '				},',
  '',
  '				getPreviousVisibleMatchIndex: function() {',
  '					for (var i = currentMatchIndex - 1; i >= 0; i--) {',
  '						if (this.matches[i].isVisible()) {',
  '							return i;',
  '						}',
  '					}',
  '					// Start again from the last match',
  '					for (var i = this.matches.length - 1; i >= currentMatchIndex; i--) {',
  '						if (this.matches[i].isVisible()) {',
  '							return i;',
  '						}',
  '					}',
  '					return -1;',
  '				},',
  '',
  '				applyTo: function(logEntry) {',
  '					var doesMatch = this.match(logEntry);',
  '					if (doesMatch) {',
  '						logEntry.group.expand();',
  '						logEntry.setSearchMatch(true);',
  '						var logEntryContent;',
  '						var wrappedLogEntryContent;',
  '						var searchTermReplacementStartTag = "<span class=\\\"searchterm\\\">";',
  '						var searchTermReplacementEndTag = "<" + "/span>";',
  '						var preTagName = isIe ? "pre" : "span";',
  '						var preStartTag = "<" + preTagName + " class=\\\"pre\\\">";',
  '						var preEndTag = "<" + "/" + preTagName + ">";',
  '						var startIndex = 0;',
  '						var searchIndex, matchedText, textBeforeMatch;',
  '						if (this.isRegex) {',
  '							var flags = this.isCaseSensitive ? "g" : "gi";',
  '							var capturingRegex = new RegExp("(" + this.searchRegex.source + ")", flags);',
  '',
  '							// Replace the search term with temporary tokens for the start and end tags',
  '							var rnd = ("" + Math.random()).substr(2);',
  '							var startToken = "%%s" + rnd + "%%";',
  '							var endToken = "%%e" + rnd + "%%";',
  '							logEntryContent = logEntry.formattedMessage.replace(capturingRegex, startToken + "$1" + endToken);',
  '',
  '							// Escape the HTML to get rid of angle brackets',
  '							logEntryContent = escapeHtml(logEntryContent);',
  '',
  '							// Substitute the proper HTML back in for the search match',
  '							var result;',
  '							var searchString = logEntryContent;',
  '							logEntryContent = "";',
  '							wrappedLogEntryContent = "";',
  '							while ((searchIndex = searchString.indexOf(startToken, startIndex)) > -1) {',
  '								var endTokenIndex = searchString.indexOf(endToken, searchIndex);',
  '								matchedText = searchString.substring(searchIndex + startToken.length, endTokenIndex);',
  '								textBeforeMatch = searchString.substring(startIndex, searchIndex);',
  '								logEntryContent += preStartTag + textBeforeMatch + preEndTag;',
  '								logEntryContent += searchTermReplacementStartTag + preStartTag + matchedText +',
  '									preEndTag + searchTermReplacementEndTag;',
  '								if (isIe) {',
  '									wrappedLogEntryContent += textBeforeMatch + searchTermReplacementStartTag +',
  '										matchedText + searchTermReplacementEndTag;',
  '								}',
  '								startIndex = endTokenIndex + endToken.length;',
  '							}',
  '							logEntryContent += preStartTag + searchString.substr(startIndex) + preEndTag;',
  '							if (isIe) {',
  '								wrappedLogEntryContent += searchString.substr(startIndex);',
  '							}',
  '						} else {',
  '							logEntryContent = "";',
  '							wrappedLogEntryContent = "";',
  '							var searchTermReplacementLength = searchTermReplacementStartTag.length +',
  '								this.searchTerm.length + searchTermReplacementEndTag.length;',
  '							var searchTermLength = this.searchTerm.length;',
  '							var searchTermLowerCase = this.searchTerm.toLowerCase();',
  '							var logTextLowerCase = logEntry.formattedMessage.toLowerCase();',
  '							while ((searchIndex = logTextLowerCase.indexOf(searchTermLowerCase, startIndex)) > -1) {',
  '								matchedText = escapeHtml(logEntry.formattedMessage.substr(searchIndex, this.searchTerm.length));',
  '								textBeforeMatch = escapeHtml(logEntry.formattedMessage.substring(startIndex, searchIndex));',
  '								var searchTermReplacement = searchTermReplacementStartTag +',
  '									preStartTag + matchedText + preEndTag + searchTermReplacementEndTag;',
  '								logEntryContent += preStartTag + textBeforeMatch + preEndTag + searchTermReplacement;',
  '								if (isIe) {',
  '									wrappedLogEntryContent += textBeforeMatch + searchTermReplacementStartTag +',
  '										matchedText + searchTermReplacementEndTag;',
  '								}',
  '								startIndex = searchIndex + searchTermLength;',
  '							}',
  '							var textAfterLastMatch = escapeHtml(logEntry.formattedMessage.substr(startIndex));',
  '							logEntryContent += preStartTag + textAfterLastMatch + preEndTag;',
  '							if (isIe) {',
  '								wrappedLogEntryContent += textAfterLastMatch;',
  '							}',
  '						}',
  '						logEntry.setContent(logEntryContent, wrappedLogEntryContent);',
  '						var logEntryMatches = logEntry.getSearchMatches();',
  '						this.matches = this.matches.concat(logEntryMatches);',
  '					} else {',
  '						logEntry.setSearchMatch(false);',
  '						logEntry.setContent(logEntry.formattedMessage, logEntry.formattedMessage);',
  '					}',
  '					return doesMatch;',
  '				},',
  '',
  '				removeMatches: function(logEntries) {',
  '					var matchesToRemoveCount = 0;',
  '					var currentMatchRemoved = false;',
  '					var matchesToRemove = [];',
  '					var i, iLen, j, jLen;',
  '',
  '					// Establish the list of matches to be removed',
  '					for (i = 0, iLen = this.matches.length; i < iLen; i++) {',
  '						for (j = 0, jLen = logEntries.length; j < jLen; j++) {',
  '							if (this.matches[i].belongsTo(logEntries[j])) {',
  '								matchesToRemove.push(this.matches[i]);',
  '								if (i === currentMatchIndex) {',
  '									currentMatchRemoved = true;',
  '								}',
  '							}',
  '						}',
  '					}',
  '',
  '					// Set the new current match index if the current match has been deleted',
  '					// This will be the first match that appears after the first log entry being',
  '					// deleted, if one exists; otherwise, it\'s the first match overall',
  '					var newMatch = currentMatchRemoved ? null : this.matches[currentMatchIndex];',
  '					if (currentMatchRemoved) {',
  '						for (i = currentMatchIndex, iLen = this.matches.length; i < iLen; i++) {',
  '							if (this.matches[i].isVisible() && !array_contains(matchesToRemove, this.matches[i])) {',
  '								newMatch = this.matches[i];',
  '								break;',
  '							}',
  '						}',
  '					}',
  '',
  '					// Remove the matches',
  '					for (i = 0, iLen = matchesToRemove.length; i < iLen; i++) {',
  '						array_remove(this.matches, matchesToRemove[i]);',
  '						matchesToRemove[i].remove();',
  '					}',
  '',
  '					// Set the new match, if one exists',
  '					if (this.hasVisibleMatches()) {',
  '						if (newMatch === null) {',
  '							setCurrentMatchIndex(0);',
  '						} else {',
  '							// Get the index of the new match',
  '							var newMatchIndex = 0;',
  '							for (i = 0, iLen = this.matches.length; i < iLen; i++) {',
  '								if (newMatch === this.matches[i]) {',
  '									newMatchIndex = i;',
  '									break;',
  '								}',
  '							}',
  '							setCurrentMatchIndex(newMatchIndex);',
  '						}',
  '					} else {',
  '						currentMatchIndex = null;',
  '						displayNoMatches();',
  '					}',
  '				}',
  '			};',
  '',
  '			function getPageOffsetTop(el, container) {',
  '				var currentEl = el;',
  '				var y = 0;',
  '				while (currentEl && currentEl != container) {',
  '					y += currentEl.offsetTop;',
  '					currentEl = currentEl.offsetParent;',
  '				}',
  '				return y;',
  '			}',
  '',
  '			function scrollIntoView(el) {',
  '				var logContainer = logMainContainer;',
  '				// Check if the whole width of the element is visible and centre if not',
  '				if (!$("wrap").checked) {',
  '					var logContainerLeft = logContainer.scrollLeft;',
  '					var logContainerRight = logContainerLeft  + logContainer.offsetWidth;',
  '					var elLeft = el.offsetLeft;',
  '					var elRight = elLeft + el.offsetWidth;',
  '					if (elLeft < logContainerLeft || elRight > logContainerRight) {',
  '						logContainer.scrollLeft = elLeft - (logContainer.offsetWidth - el.offsetWidth) / 2;',
  '					}',
  '				}',
  '				// Check if the whole height of the element is visible and centre if not',
  '				var logContainerTop = logContainer.scrollTop;',
  '				var logContainerBottom = logContainerTop  + logContainer.offsetHeight;',
  '				var elTop = getPageOffsetTop(el) - getToolBarsHeight();',
  '				var elBottom = elTop + el.offsetHeight;',
  '				if (elTop < logContainerTop || elBottom > logContainerBottom) {',
  '					logContainer.scrollTop = elTop - (logContainer.offsetHeight - el.offsetHeight) / 2;',
  '				}',
  '			}',
  '',
  '			function Match(logEntryLevel, spanInMainDiv, spanInUnwrappedPre, spanInWrappedDiv) {',
  '				this.logEntryLevel = logEntryLevel;',
  '				this.spanInMainDiv = spanInMainDiv;',
  '				if (isIe) {',
  '					this.spanInUnwrappedPre = spanInUnwrappedPre;',
  '					this.spanInWrappedDiv = spanInWrappedDiv;',
  '				}',
  '				this.mainSpan = isIe ? spanInUnwrappedPre : spanInMainDiv;',
  '			}',
  '',
  '			Match.prototype = {',
  '				equals: function(match) {',
  '					return this.mainSpan === match.mainSpan;',
  '				},',
  '',
  '				setCurrent: function() {',
  '					if (isIe) {',
  '						addClass(this.spanInUnwrappedPre, "currentmatch");',
  '						addClass(this.spanInWrappedDiv, "currentmatch");',
  '						// Scroll the visible one into view',
  '						var elementToScroll = $("wrap").checked ? this.spanInWrappedDiv : this.spanInUnwrappedPre;',
  '						scrollIntoView(elementToScroll);',
  '					} else {',
  '						addClass(this.spanInMainDiv, "currentmatch");',
  '						scrollIntoView(this.spanInMainDiv);',
  '					}',
  '				},',
  '',
  '				belongsTo: function(logEntry) {',
  '					if (isIe) {',
  '						return isDescendant(this.spanInUnwrappedPre, logEntry.unwrappedPre);',
  '					} else {',
  '						return isDescendant(this.spanInMainDiv, logEntry.mainDiv);',
  '					}',
  '				},',
  '',
  '				setNotCurrent: function() {',
  '					if (isIe) {',
  '						removeClass(this.spanInUnwrappedPre, "currentmatch");',
  '						removeClass(this.spanInWrappedDiv, "currentmatch");',
  '					} else {',
  '						removeClass(this.spanInMainDiv, "currentmatch");',
  '					}',
  '				},',
  '',
  '				isOrphan: function() {',
  '					return isOrphan(this.mainSpan);',
  '				},',
  '',
  '				isVisible: function() {',
  '					return getCheckBox(this.logEntryLevel).checked;',
  '				},',
  '',
  '				remove: function() {',
  '					if (isIe) {',
  '						this.spanInUnwrappedPre = null;',
  '						this.spanInWrappedDiv = null;',
  '					} else {',
  '						this.spanInMainDiv = null;',
  '					}',
  '				}',
  '			};',
  '',
  '			var currentSearch = null;',
  '			var currentMatchIndex = null;',
  '',
  '			function doSearch() {',
  '				var searchBox = $("searchBox");',
  '				var searchTerm = searchBox.value;',
  '				var isRegex = $("searchRegex").checked;',
  '				var isCaseSensitive = $("searchCaseSensitive").checked;',
  '				var i;',
  '',
  '				if (searchTerm === "") {',
  '					$("searchReset").disabled = true;',
  '					$("searchNav").style.display = "none";',
  '					removeClass(document.body, "searching");',
  '					removeClass(searchBox, "hasmatches");',
  '					removeClass(searchBox, "nomatches");',
  '					for (i = 0; i < logEntries.length; i++) {',
  '						logEntries[i].clearSearch();',
  '						logEntries[i].setContent(logEntries[i].formattedMessage, logEntries[i].formattedMessage);',
  '					}',
  '					currentSearch = null;',
  '					setLogContainerHeight();',
  '				} else {',
  '					$("searchReset").disabled = false;',
  '					$("searchNav").style.display = "block";',
  '					var searchRegex;',
  '					var regexValid;',
  '					if (isRegex) {',
  '						try {',
  '							searchRegex = isCaseSensitive ? new RegExp(searchTerm, "g") : new RegExp(searchTerm, "gi");',
  '							regexValid = true;',
  '							replaceClass(searchBox, "validregex", "invalidregex");',
  '							searchBox.title = "Valid regex";',
  '						} catch (ex) {',
  '							regexValid = false;',
  '							replaceClass(searchBox, "invalidregex", "validregex");',
  '							searchBox.title = "Invalid regex: " + (ex.message ? ex.message : (ex.description ? ex.description : "unknown error"));',
  '							return;',
  '						}',
  '					} else {',
  '						searchBox.title = "";',
  '						removeClass(searchBox, "validregex");',
  '						removeClass(searchBox, "invalidregex");',
  '					}',
  '					addClass(document.body, "searching");',
  '					currentSearch = new Search(searchTerm, isRegex, searchRegex, isCaseSensitive);',
  '					for (i = 0; i < logEntries.length; i++) {',
  '						currentSearch.applyTo(logEntries[i]);',
  '					}',
  '					setLogContainerHeight();',
  '',
  '					// Highlight the first search match',
  '					if (currentSearch.hasVisibleMatches()) {',
  '						setCurrentMatchIndex(0);',
  '						displayMatches();',
  '					} else {',
  '						displayNoMatches();',
  '					}',
  '				}',
  '			}',
  '',
  '			function updateSearchFromFilters() {',
  '				if (currentSearch) {',
  '					if (currentSearch.hasMatches()) {',
  '						if (currentMatchIndex === null) {',
  '							currentMatchIndex = 0;',
  '						}',
  '						var currentMatch = currentSearch.matches[currentMatchIndex];',
  '						if (currentMatch.isVisible()) {',
  '							displayMatches();',
  '							setCurrentMatchIndex(currentMatchIndex);',
  '						} else {',
  '							currentMatch.setNotCurrent();',
  '							// Find the next visible match, if one exists',
  '							var nextVisibleMatchIndex = currentSearch.getNextVisibleMatchIndex();',
  '							if (nextVisibleMatchIndex > -1) {',
  '								setCurrentMatchIndex(nextVisibleMatchIndex);',
  '								displayMatches();',
  '							} else {',
  '								displayNoMatches();',
  '							}',
  '						}',
  '					} else {',
  '						displayNoMatches();',
  '					}',
  '				}',
  '			}',
  '',
  '			function refreshCurrentMatch() {',
  '				if (currentSearch && currentSearch.hasVisibleMatches()) {',
  '					setCurrentMatchIndex(currentMatchIndex);',
  '				}',
  '			}',
  '',
  '			function displayMatches() {',
  '				replaceClass($("searchBox"), "hasmatches", "nomatches");',
  '				$("searchBox").title = "" + currentSearch.matches.length + " matches found";',
  '				$("searchNav").style.display = "block";',
  '				setLogContainerHeight();',
  '			}',
  '',
  '			function displayNoMatches() {',
  '				replaceClass($("searchBox"), "nomatches", "hasmatches");',
  '				$("searchBox").title = "No matches found";',
  '				$("searchNav").style.display = "none";',
  '				setLogContainerHeight();',
  '			}',
  '',
  '			function toggleSearchEnabled(enable) {',
  '				enable = (typeof enable == "undefined") ? !$("searchDisable").checked : enable;',
  '				$("searchBox").disabled = !enable;',
  '				$("searchReset").disabled = !enable;',
  '				$("searchRegex").disabled = !enable;',
  '				$("searchNext").disabled = !enable;',
  '				$("searchPrevious").disabled = !enable;',
  '				$("searchCaseSensitive").disabled = !enable;',
  '				$("searchNav").style.display = (enable && ($("searchBox").value !== "") &&',
  '						currentSearch && currentSearch.hasVisibleMatches()) ?',
  '					"block" : "none";',
  '				if (enable) {',
  '					removeClass($("search"), "greyedout");',
  '					addClass(document.body, "searching");',
  '					if ($("searchHighlight").checked) {',
  '						addClass(logMainContainer, "searchhighlight");',
  '					} else {',
  '						removeClass(logMainContainer, "searchhighlight");',
  '					}',
  '					if ($("searchFilter").checked) {',
  '						addClass(logMainContainer, "searchfilter");',
  '					} else {',
  '						removeClass(logMainContainer, "searchfilter");',
  '					}',
  '					$("searchDisable").checked = !enable;',
  '				} else {',
  '					addClass($("search"), "greyedout");',
  '					removeClass(document.body, "searching");',
  '					removeClass(logMainContainer, "searchhighlight");',
  '					removeClass(logMainContainer, "searchfilter");',
  '				}',
  '				setLogContainerHeight();',
  '			}',
  '',
  '			function toggleSearchFilter() {',
  '				var enable = $("searchFilter").checked;',
  '				if (enable) {',
  '					addClass(logMainContainer, "searchfilter");',
  '				} else {',
  '					removeClass(logMainContainer, "searchfilter");',
  '				}',
  '				refreshCurrentMatch();',
  '			}',
  '',
  '			function toggleSearchHighlight() {',
  '				var enable = $("searchHighlight").checked;',
  '				if (enable) {',
  '					addClass(logMainContainer, "searchhighlight");',
  '				} else {',
  '					removeClass(logMainContainer, "searchhighlight");',
  '				}',
  '			}',
  '',
  '			function clearSearch() {',
  '				$("searchBox").value = "";',
  '				doSearch();',
  '			}',
  '',
  '			function searchNext() {',
  '				if (currentSearch !== null && currentMatchIndex !== null) {',
  '					currentSearch.matches[currentMatchIndex].setNotCurrent();',
  '					var nextMatchIndex = currentSearch.getNextVisibleMatchIndex();',
  '					if (nextMatchIndex > currentMatchIndex || confirm("Reached the end of the page. Start from the top?")) {',
  '						setCurrentMatchIndex(nextMatchIndex);',
  '					}',
  '				}',
  '			}',
  '',
  '			function searchPrevious() {',
  '				if (currentSearch !== null && currentMatchIndex !== null) {',
  '					currentSearch.matches[currentMatchIndex].setNotCurrent();',
  '					var previousMatchIndex = currentSearch.getPreviousVisibleMatchIndex();',
  '					if (previousMatchIndex < currentMatchIndex || confirm("Reached the start of the page. Continue from the bottom?")) {',
  '						setCurrentMatchIndex(previousMatchIndex);',
  '					}',
  '				}',
  '			}',
  '',
  '			function setCurrentMatchIndex(index) {',
  '				currentMatchIndex = index;',
  '				currentSearch.matches[currentMatchIndex].setCurrent();',
  '			}',
  '',
  '			/* ------------------------------------------------------------------------- */',
  '',
  '			// CSS Utilities',
  '',
  '			function addClass(el, cssClass) {',
  '				if (!hasClass(el, cssClass)) {',
  '					if (el.className) {',
  '						el.className += " " + cssClass;',
  '					} else {',
  '						el.className = cssClass;',
  '					}',
  '				}',
  '			}',
  '',
  '			function hasClass(el, cssClass) {',
  '				if (el.className) {',
  '					var classNames = el.className.split(" ");',
  '					return array_contains(classNames, cssClass);',
  '				}',
  '				return false;',
  '			}',
  '',
  '			function removeClass(el, cssClass) {',
  '				if (hasClass(el, cssClass)) {',
  '					// Rebuild the className property',
  '					var existingClasses = el.className.split(" ");',
  '					var newClasses = [];',
  '					for (var i = 0, len = existingClasses.length; i < len; i++) {',
  '						if (existingClasses[i] != cssClass) {',
  '							newClasses[newClasses.length] = existingClasses[i];',
  '						}',
  '					}',
  '					el.className = newClasses.join(" ");',
  '				}',
  '			}',
  '',
  '			function replaceClass(el, newCssClass, oldCssClass) {',
  '				removeClass(el, oldCssClass);',
  '				addClass(el, newCssClass);',
  '			}',
  '',
  '			/* ------------------------------------------------------------------------- */',
  '',
  '			// Other utility functions',
  '',
  '			function getElementsByClass(el, cssClass, tagName) {',
  '				var elements = el.getElementsByTagName(tagName);',
  '				var matches = [];',
  '				for (var i = 0, len = elements.length; i < len; i++) {',
  '					if (hasClass(elements[i], cssClass)) {',
  '						matches.push(elements[i]);',
  '					}',
  '				}',
  '				return matches;',
  '			}',
  '',
  '			// Syntax borrowed from Prototype library',
  '			function $(id) {',
  '				return document.getElementById(id);',
  '			}',
  '',
  '			function isDescendant(node, ancestorNode) {',
  '				while (node != null) {',
  '					if (node === ancestorNode) {',
  '						return true;',
  '					}',
  '					node = node.parentNode;',
  '				}',
  '				return false;',
  '			}',
  '',
  '			function isOrphan(node) {',
  '				var currentNode = node;',
  '				while (currentNode) {',
  '					if (currentNode == document.body) {',
  '						return false;',
  '					}',
  '					currentNode = currentNode.parentNode;',
  '				}',
  '				return true;',
  '			}',
  '',
  '			function escapeHtml(str) {',
  '				return str.replace(/&/g, "&amp;").replace(/[<]/g, "&lt;").replace(/>/g, "&gt;");',
  '			}',
  '',
  '			function getWindowWidth() {',
  '				if (window.innerWidth) {',
  '					return window.innerWidth;',
  '				} else if (document.documentElement && document.documentElement.clientWidth) {',
  '					return document.documentElement.clientWidth;',
  '				} else if (document.body) {',
  '					return document.body.clientWidth;',
  '				}',
  '				return 0;',
  '			}',
  '',
  '			function getWindowHeight() {',
  '				if (window.innerHeight) {',
  '					return window.innerHeight;',
  '				} else if (document.documentElement && document.documentElement.clientHeight) {',
  '					return document.documentElement.clientHeight;',
  '				} else if (document.body) {',
  '					return document.body.clientHeight;',
  '				}',
  '				return 0;',
  '			}',
  '',
  '			function getToolBarsHeight() {',
  '				return $("switches").offsetHeight;',
  '			}',
  '',
  '			function getChromeHeight() {',
  '				var height = getToolBarsHeight();',
  '				if (showCommandLine) {',
  '					height += $("commandLine").offsetHeight;',
  '				}',
  '				return height;',
  '			}',
  '',
  '			function setLogContainerHeight() {',
  '				if (logMainContainer) {',
  '					var windowHeight = getWindowHeight();',
  '					$("body").style.height = getWindowHeight() + "px";',
  '					logMainContainer.style.height = "" +',
  '						Math.max(0, windowHeight - getChromeHeight()) + "px";',
  '				}',
  '			}',
  '',
  '			function setCommandInputWidth() {',
  '				if (showCommandLine) {',
  '					$("command").style.width = "" + Math.max(0, $("commandLineContainer").offsetWidth -',
  '						($("evaluateButton").offsetWidth + 13)) + "px";',
  '				}',
  '			}',
  '',
  '			window.onresize = function() {',
  '				setCommandInputWidth();',
  '				setLogContainerHeight();',
  '			};',
  '',
  '			if (!Array.prototype.push) {',
  '				Array.prototype.push = function() {',
  '			        for (var i = 0, len = arguments.length; i < len; i++){',
  '			            this[this.length] = arguments[i];',
  '			        }',
  '			        return this.length;',
  '				};',
  '			}',
  '',
  '			if (!Array.prototype.pop) {',
  '				Array.prototype.pop = function() {',
  '					if (this.length > 0) {',
  '						var val = this[this.length - 1];',
  '						this.length = this.length - 1;',
  '						return val;',
  '					}',
  '				};',
  '			}',
  '',
  '			if (!Array.prototype.shift) {',
  '				Array.prototype.shift = function() {',
  '					if (this.length > 0) {',
  '						var firstItem = this[0];',
  '						for (var i = 0, len = this.length - 1; i < len; i++) {',
  '							this[i] = this[i + 1];',
  '						}',
  '						this.length = this.length - 1;',
  '						return firstItem;',
  '					}',
  '				};',
  '			}',
  '',
  '			if (!Array.prototype.splice) {',
  '				Array.prototype.splice = function(startIndex, deleteCount) {',
  '					var itemsAfterDeleted = this.slice(startIndex + deleteCount);',
  '					var itemsDeleted = this.slice(startIndex, startIndex + deleteCount);',
  '					this.length = startIndex;',
  '					// Copy the arguments into a proper Array object',
  '					var argumentsArray = [];',
  '					for (var i = 0, len = arguments.length; i < len; i++) {',
  '						argumentsArray[i] = arguments[i];',
  '					}',
  '					var itemsToAppend = (argumentsArray.length > 2) ?',
  '						itemsAfterDeleted = argumentsArray.slice(2).concat(itemsAfterDeleted) : itemsAfterDeleted;',
  '					for (i = 0, len = itemsToAppend.length; i < len; i++) {',
  '						this.push(itemsToAppend[i]);',
  '					}',
  '					return itemsDeleted;',
  '				};',
  '			}',
  '',
  '			function array_remove(arr, val) {',
  '				var index = -1;',
  '				for (var i = 0, len = arr.length; i < len; i++) {',
  '					if (arr[i] === val) {',
  '						index = i;',
  '						break;',
  '					}',
  '				}',
  '				if (index >= 0) {',
  '					arr.splice(index, 1);',
  '					return index;',
  '				} else {',
  '					return false;',
  '				}',
  '			}',
  '',
  '			function array_removeFromStart(array, numberToRemove) {',
  '				if (Array.prototype.splice) {',
  '					array.splice(0, numberToRemove);',
  '				} else {',
  '					for (var i = numberToRemove, len = array.length; i < len; i++) {',
  '						array[i - numberToRemove] = array[i];',
  '					}',
  '					array.length = array.length - numberToRemove;',
  '				}',
  '				return array;',
  '			}',
  '',
  '			function array_contains(arr, val) {',
  '				for (var i = 0, len = arr.length; i < len; i++) {',
  '					if (arr[i] == val) {',
  '						return true;',
  '					}',
  '				}',
  '				return false;',
  '			}',
  '',
  '			function getErrorMessage(ex) {',
  '				if (ex.message) {',
  '					return ex.message;',
  '				} else if (ex.description) {',
  '					return ex.description;',
  '				}',
  '				return "" + ex;',
  '			}',
  '',
  '			function moveCaretToEnd(input) {',
  '				if (input.setSelectionRange) {',
  '					input.focus();',
  '					var length = input.value.length;',
  '					input.setSelectionRange(length, length);',
  '				} else if (input.createTextRange) {',
  '					var range = input.createTextRange();',
  '					range.collapse(false);',
  '					range.select();',
  '				}',
  '				input.focus();',
  '			}',
  '',
  '			function stopPropagation(evt) {',
  '				if (evt.stopPropagation) {',
  '					evt.stopPropagation();',
  '				} else if (typeof evt.cancelBubble != "undefined") {',
  '					evt.cancelBubble = true;',
  '				}',
  '			}',
  '',
  '			function getEvent(evt) {',
  '				return evt ? evt : event;',
  '			}',
  '',
  '			function getTarget(evt) {',
  '				return evt.target ? evt.target : evt.srcElement;',
  '			}',
  '',
  '			function getRelatedTarget(evt) {',
  '				if (evt.relatedTarget) {',
  '					return evt.relatedTarget;',
  '				} else if (evt.srcElement) {',
  '					switch(evt.type) {',
  '						case "mouseover":',
  '							return evt.fromElement;',
  '						case "mouseout":',
  '							return evt.toElement;',
  '						default:',
  '							return evt.srcElement;',
  '					}',
  '				}',
  '			}',
  '',
  '			function cancelKeyEvent(evt) {',
  '				evt.returnValue = false;',
  '				stopPropagation(evt);',
  '			}',
  '',
  '			function evalCommandLine() {',
  '				var expr = $("command").value;',
  '				evalCommand(expr);',
  '				$("command").value = "";',
  '			}',
  '',
  '			function evalLastCommand() {',
  '				if (lastCommand != null) {',
  '					evalCommand(lastCommand);',
  '				}',
  '			}',
  '',
  '			var lastCommand = null;',
  '			var commandHistory = [];',
  '			var currentCommandIndex = 0;',
  '',
  '			function evalCommand(expr) {',
  '				if (appender) {',
  '					appender.evalCommandAndAppend(expr);',
  '				} else {',
  '					var prefix = ">>> " + expr + "\\r\\n";',
  '					try {',
  '						log("INFO", prefix + eval(expr));',
  '					} catch (ex) {',
  '						log("ERROR", prefix + "Error: " + getErrorMessage(ex));',
  '					}',
  '				}',
  '				// Update command history',
  '				if (expr != commandHistory[commandHistory.length - 1]) {',
  '					commandHistory.push(expr);',
  '					// Update the appender',
  '					if (appender) {',
  '						appender.storeCommandHistory(commandHistory);',
  '					}',
  '				}',
  '				currentCommandIndex = (expr == commandHistory[currentCommandIndex]) ? currentCommandIndex + 1 : commandHistory.length;',
  '				lastCommand = expr;',
  '			}',
  '			//]]>',
  '		</script>',
  '		<style type="text/css">',
  '			body {',
  '				background-color: white;',
  '				color: black;',
  '				padding: 0;',
  '				margin: 0;',
  '				font-family: tahoma, verdana, arial, helvetica, sans-serif;',
  '				overflow: hidden;',
  '			}',
  '',
  '			div#switchesContainer input {',
  '				margin-bottom: 0;',
  '			}',
  '',
  '			div.toolbar {',
  '				border-top: solid #ffffff 1px;',
  '				border-bottom: solid #aca899 1px;',
  '				background-color: #f1efe7;',
  '				padding: 3px 5px;',
  '				font-size: 68.75%;',
  '			}',
  '',
  '			div.toolbar, div#search input {',
  '				font-family: tahoma, verdana, arial, helvetica, sans-serif;',
  '			}',
  '',
  '			div.toolbar input.button {',
  '				padding: 0 5px;',
  '				font-size: 100%;',
  '			}',
  '',
  '			div.toolbar input.hidden {',
  '				display: none;',
  '			}',
  '',
  '			div#switches input#clearButton {',
  '				margin-left: 20px;',
  '			}',
  '',
  '			div#levels label {',
  '				font-weight: bold;',
  '			}',
  '',
  '			div#levels label, div#options label {',
  '				margin-right: 5px;',
  '			}',
  '',
  '			div#levels label#wrapLabel {',
  '				font-weight: normal;',
  '			}',
  '',
  '			div#search label {',
  '				margin-right: 10px;',
  '			}',
  '',
  '			div#search label.searchboxlabel {',
  '				margin-right: 0;',
  '			}',
  '',
  '			div#search input {',
  '				font-size: 100%;',
  '			}',
  '',
  '			div#search input.validregex {',
  '				color: green;',
  '			}',
  '',
  '			div#search input.invalidregex {',
  '				color: red;',
  '			}',
  '',
  '			div#search input.nomatches {',
  '				color: white;',
  '				background-color: #ff6666;',
  '			}',
  '',
  '			div#search input.nomatches {',
  '				color: white;',
  '				background-color: #ff6666;',
  '			}',
  '',
  '			div#searchNav {',
  '				display: none;',
  '			}',
  '',
  '			div#commandLine {',
  '				display: none;',
  '			}',
  '',
  '			div#commandLine input#command {',
  '				font-size: 100%;',
  '				font-family: Courier New, Courier;',
  '			}',
  '',
  '			div#commandLine input#evaluateButton {',
  '			}',
  '',
  '			*.greyedout {',
  '				color: gray !important;',
  '				border-color: gray !important;',
  '			}',
  '',
  '			*.greyedout *.alwaysenabled { color: black; }',
  '',
  '			*.unselectable {',
  '				-khtml-user-select: none;',
  '				-moz-user-select: none;',
  '				user-select: none;',
  '			}',
  '',
  '			div#log {',
  '				font-family: Courier New, Courier;',
  '				font-size: 75%;',
  '				width: 100%;',
  '				overflow: auto;',
  '				clear: both;',
  '				position: relative;',
  '			}',
  '',
  '			div.group {',
  '				border-color: #cccccc;',
  '				border-style: solid;',
  '				border-width: 1px 0 1px 1px;',
  '				overflow: visible;',
  '			}',
  '',
  '			div.oldIe div.group, div.oldIe div.group *, div.oldIe *.logentry {',
  '				height: 1%;',
  '			}',
  '',
  '			div.group div.groupheading span.expander {',
  '				border: solid black 1px;',
  '				font-family: Courier New, Courier;',
  '				font-size: 0.833em;',
  '				background-color: #eeeeee;',
  '				position: relative;',
  '				top: -1px;',
  '				color: black;',
  '				padding: 0 2px;',
  '				cursor: pointer;',
  '				cursor: hand;',
  '				height: 1%;',
  '			}',
  '',
  '			div.group div.groupcontent {',
  '				margin-left: 10px;',
  '				padding-bottom: 2px;',
  '				overflow: visible;',
  '			}',
  '',
  '			div.group div.expanded {',
  '				display: block;',
  '			}',
  '',
  '			div.group div.collapsed {',
  '				display: none;',
  '			}',
  '',
  '			*.logentry {',
  '				overflow: visible;',
  '				display: none;',
  '				white-space: pre;',
  '			}',
  '',
  '			span.pre {',
  '				white-space: pre;',
  '			}',
  '			',
  '			pre.unwrapped {',
  '				display: inline !important;',
  '			}',
  '',
  '			pre.unwrapped pre.pre, div.wrapped pre.pre {',
  '				display: inline;',
  '			}',
  '',
  '			div.wrapped pre.pre {',
  '				white-space: normal;',
  '			}',
  '',
  '			div.wrapped {',
  '				display: none;',
  '			}',
  '',
  '			body.searching *.logentry span.currentmatch {',
  '				color: white !important;',
  '				background-color: green !important;',
  '			}',
  '',
  '			body.searching div.searchhighlight *.logentry span.searchterm {',
  '				color: black;',
  '				background-color: yellow;',
  '			}',
  '',
  '			div.wrap *.logentry {',
  '				white-space: normal !important;',
  '				border-width: 0 0 1px 0;',
  '				border-color: #dddddd;',
  '				border-style: dotted;',
  '			}',
  '',
  '			div.wrap #log_wrapped, #log_unwrapped {',
  '				display: block;',
  '			}',
  '',
  '			div.wrap #log_unwrapped, #log_wrapped {',
  '				display: none;',
  '			}',
  '',
  '			div.wrap *.logentry span.pre {',
  '				overflow: visible;',
  '				white-space: normal;',
  '			}',
  '',
  '			div.wrap *.logentry pre.unwrapped {',
  '				display: none;',
  '			}',
  '',
  '			div.wrap *.logentry span.wrapped {',
  '				display: inline;',
  '			}',
  '',
  '			div.searchfilter *.searchnonmatch {',
  '				display: none !important;',
  '			}',
  '',
  '			div#log *.TRACE, label#label_TRACE {',
  '				color: #666666;',
  '			}',
  '',
  '			div#log *.DEBUG, label#label_DEBUG {',
  '				color: green;',
  '			}',
  '',
  '			div#log *.INFO, label#label_INFO {',
  '				color: #000099;',
  '			}',
  '',
  '			div#log *.WARN, label#label_WARN {',
  '				color: #999900;',
  '			}',
  '',
  '			div#log *.ERROR, label#label_ERROR {',
  '				color: red;',
  '			}',
  '',
  '			div#log *.FATAL, label#label_FATAL {',
  '				color: #660066;',
  '			}',
  '',
  '			div.TRACE#log *.TRACE,',
  '			div.DEBUG#log *.DEBUG,',
  '			div.INFO#log *.INFO,',
  '			div.WARN#log *.WARN,',
  '			div.ERROR#log *.ERROR,',
  '			div.FATAL#log *.FATAL {',
  '				display: block;',
  '			}',
  '',
  '			div#log div.separator {',
  '				background-color: #cccccc;',
  '				margin: 5px 0;',
  '				line-height: 1px;',
  '			}',
  '		</style>',
  '	</head>',
  '',
  '	<body id="body">',
  '		<div id="switchesContainer">',
  '			<div id="switches">',
  '				<div id="levels" class="toolbar">',
  '					Filters:',
  '					<input type="checkbox" id="switch_TRACE" onclick="applyFilters(); checkAllLevels()" checked="checked" title="Show/hide trace messages" /><label for="switch_TRACE" id="label_TRACE">trace</label>',
  '					<input type="checkbox" id="switch_DEBUG" onclick="applyFilters(); checkAllLevels()" checked="checked" title="Show/hide debug messages" /><label for="switch_DEBUG" id="label_DEBUG">debug</label>',
  '					<input type="checkbox" id="switch_INFO" onclick="applyFilters(); checkAllLevels()" checked="checked" title="Show/hide info messages" /><label for="switch_INFO" id="label_INFO">info</label>',
  '					<input type="checkbox" id="switch_WARN" onclick="applyFilters(); checkAllLevels()" checked="checked" title="Show/hide warn messages" /><label for="switch_WARN" id="label_WARN">warn</label>',
  '					<input type="checkbox" id="switch_ERROR" onclick="applyFilters(); checkAllLevels()" checked="checked" title="Show/hide error messages" /><label for="switch_ERROR" id="label_ERROR">error</label>',
  '					<input type="checkbox" id="switch_FATAL" onclick="applyFilters(); checkAllLevels()" checked="checked" title="Show/hide fatal messages" /><label for="switch_FATAL" id="label_FATAL">fatal</label>',
  '					<input type="checkbox" id="switch_ALL" onclick="toggleAllLevels(); applyFilters()" checked="checked" title="Show/hide all messages" /><label for="switch_ALL" id="label_ALL">all</label>',
  '				</div>',
  '				<div id="search" class="toolbar">',
  '					<label for="searchBox" class="searchboxlabel">Search:</label> <input type="text" id="searchBox" onclick="toggleSearchEnabled(true)" onkeyup="scheduleSearch()" size="20" />',
  '					<input type="button" id="searchReset" disabled="disabled" value="Reset" onclick="clearSearch()" class="button" title="Reset the search" />',
  '					<input type="checkbox" id="searchRegex" onclick="doSearch()" title="If checked, search is treated as a regular expression" /><label for="searchRegex">Regex</label>',
  '					<input type="checkbox" id="searchCaseSensitive" onclick="doSearch()" title="If checked, search is case sensitive" /><label for="searchCaseSensitive">Match case</label>',
  '					<input type="checkbox" id="searchDisable" onclick="toggleSearchEnabled()" title="Enable/disable search" /><label for="searchDisable" class="alwaysenabled">Disable</label>',
  '					<div id="searchNav">',
  '						<input type="button" id="searchNext" disabled="disabled" value="Next" onclick="searchNext()" class="button" title="Go to the next matching log entry" />',
  '						<input type="button" id="searchPrevious" disabled="disabled" value="Previous" onclick="searchPrevious()" class="button" title="Go to the previous matching log entry" />',
  '						<input type="checkbox" id="searchFilter" onclick="toggleSearchFilter()" title="If checked, non-matching log entries are filtered out" /><label for="searchFilter">Filter</label>',
  '						<input type="checkbox" id="searchHighlight" onclick="toggleSearchHighlight()" title="Highlight matched search terms" /><label for="searchHighlight" class="alwaysenabled">Highlight all</label>',
  '					</div>',
  '				</div>',
  '				<div id="options" class="toolbar">',
  '					Options:',
  '					<input type="checkbox" id="enableLogging" onclick="toggleLoggingEnabled()" checked="checked" title="Enable/disable logging" /><label for="enableLogging" id="enableLoggingLabel">Log</label>',
  '					<input type="checkbox" id="wrap" onclick="toggleWrap()" title="Enable / disable word wrap" /><label for="wrap" id="wrapLabel">Wrap</label>',
  '					<input type="checkbox" id="newestAtTop" onclick="toggleNewestAtTop()" title="If checked, causes newest messages to appear at the top" /><label for="newestAtTop" id="newestAtTopLabel">Newest at the top</label>',
  '					<input type="checkbox" id="scrollToLatest" onclick="toggleScrollToLatest()" checked="checked" title="If checked, window automatically scrolls to a new message when it is added" /><label for="scrollToLatest" id="scrollToLatestLabel">Scroll to latest</label>',
  '					<input type="button" id="clearButton" value="Clear" onclick="clearLog()" class="button" title="Clear all log messages"  />',
  '					<input type="button" id="hideButton" value="Hide" onclick="hide()" class="hidden button" title="Hide the console" />',
  '					<input type="button" id="closeButton" value="Close" onclick="closeWindow()" class="hidden button" title="Close the window" />',
  '				</div>',
  '			</div>',
  '		</div>',
  '		<div id="log" class="TRACE DEBUG INFO WARN ERROR FATAL"></div>',
  '		<div id="commandLine" class="toolbar">',
  '			<div id="commandLineContainer">',
  '				<input type="text" id="command" title="Enter a JavaScript command here and hit return or press \'Evaluate\'" />',
  '				<input type="button" id="evaluateButton" value="Evaluate" class="button" title="Evaluate the command" onclick="evalCommandLine()" />',
  '			</div>',
  '		</div>',
  '	</body>',
  '</html>',
  ''
  ];
  		};
  
  		var defaultCommandLineFunctions = [];
  
  		ConsoleAppender = function() {};
  
  		var consoleAppenderIdCounter = 1;
  		ConsoleAppender.prototype = new Appender();
  
  		ConsoleAppender.prototype.create = function(inPage, container,
  				lazyInit, initiallyMinimized, useDocumentWrite, width, height, focusConsoleWindow) {
  			var appender = this;
  
  			// Common properties
  			var initialized = false;
  			var consoleWindowCreated = false;
  			var consoleWindowLoaded = false;
  			var consoleClosed = false;
  
  			var queuedLoggingEvents = [];
  			var isSupported = true;
  			var consoleAppenderId = consoleAppenderIdCounter++;
  
  			// Local variables
  			initiallyMinimized = extractBooleanFromParam(initiallyMinimized, this.defaults.initiallyMinimized);
  			lazyInit = extractBooleanFromParam(lazyInit, this.defaults.lazyInit);
  			useDocumentWrite = extractBooleanFromParam(useDocumentWrite, this.defaults.useDocumentWrite);
  			var newestMessageAtTop = this.defaults.newestMessageAtTop;
  			var scrollToLatestMessage = this.defaults.scrollToLatestMessage;
  			width = width ? width : this.defaults.width;
  			height = height ? height : this.defaults.height;
  			var maxMessages = this.defaults.maxMessages;
  			var showCommandLine = this.defaults.showCommandLine;
  			var commandLineObjectExpansionDepth = this.defaults.commandLineObjectExpansionDepth;
  			var showHideButton = this.defaults.showHideButton;
  			var showCloseButton = this.defaults.showCloseButton;
  
  			this.setLayout(this.defaults.layout);
  
  			// Functions whose implementations vary between subclasses
  			var init, createWindow, safeToAppend, getConsoleWindow, open;
  
  			// Configuration methods. The function scope is used to prevent
  			// direct alteration to the appender configuration properties.
  			var appenderName = inPage ? "InPageAppender" : "PopUpAppender";
  			var checkCanConfigure = function(configOptionName) {
  				if (consoleWindowCreated) {
  					handleError(appenderName + ": configuration option '" + configOptionName + "' may not be set after the appender has been initialized");
  					return false;
  				}
  				return true;
  			};
  
  			var consoleWindowExists = function() {
  				return (consoleWindowLoaded && isSupported && !consoleClosed);
  			};
  
  			this.isNewestMessageAtTop = function() { return newestMessageAtTop; };
  			this.setNewestMessageAtTop = function(newestMessageAtTopParam) {
  				newestMessageAtTop = bool(newestMessageAtTopParam);
  				if (consoleWindowExists()) {
  					getConsoleWindow().setNewestAtTop(newestMessageAtTop);
  				}
  			};
  
  			this.isScrollToLatestMessage = function() { return scrollToLatestMessage; };
  			this.setScrollToLatestMessage = function(scrollToLatestMessageParam) {
  				scrollToLatestMessage = bool(scrollToLatestMessageParam);
  				if (consoleWindowExists()) {
  					getConsoleWindow().setScrollToLatest(scrollToLatestMessage);
  				}
  			};
  
  			this.getWidth = function() { return width; };
  			this.setWidth = function(widthParam) {
  				if (checkCanConfigure("width")) {
  					width = extractStringFromParam(widthParam, width);
  				}
  			};
  
  			this.getHeight = function() { return height; };
  			this.setHeight = function(heightParam) {
  				if (checkCanConfigure("height")) {
  					height = extractStringFromParam(heightParam, height);
  				}
  			};
  
  			this.getMaxMessages = function() { return maxMessages; };
  			this.setMaxMessages = function(maxMessagesParam) {
  				maxMessages = extractIntFromParam(maxMessagesParam, maxMessages);
  				if (consoleWindowExists()) {
  					getConsoleWindow().setMaxMessages(maxMessages);
  				}
  			};
  
  			this.isShowCommandLine = function() { return showCommandLine; };
  			this.setShowCommandLine = function(showCommandLineParam) {
  				showCommandLine = bool(showCommandLineParam);
  				if (consoleWindowExists()) {
  					getConsoleWindow().setShowCommandLine(showCommandLine);
  				}
  			};
  
  			this.isShowHideButton = function() { return showHideButton; };
  			this.setShowHideButton = function(showHideButtonParam) {
  				showHideButton = bool(showHideButtonParam);
  				if (consoleWindowExists()) {
  					getConsoleWindow().setShowHideButton(showHideButton);
  				}
  			};
  
  			this.isShowCloseButton = function() { return showCloseButton; };
  			this.setShowCloseButton = function(showCloseButtonParam) {
  				showCloseButton = bool(showCloseButtonParam);
  				if (consoleWindowExists()) {
  					getConsoleWindow().setShowCloseButton(showCloseButton);
  				}
  			};
  
  			this.getCommandLineObjectExpansionDepth = function() { return commandLineObjectExpansionDepth; };
  			this.setCommandLineObjectExpansionDepth = function(commandLineObjectExpansionDepthParam) {
  				commandLineObjectExpansionDepth = extractIntFromParam(commandLineObjectExpansionDepthParam, commandLineObjectExpansionDepth);
  			};
  
  			var minimized = initiallyMinimized;
  			this.isInitiallyMinimized = function() { return initiallyMinimized; };
  			this.setInitiallyMinimized = function(initiallyMinimizedParam) {
  				if (checkCanConfigure("initiallyMinimized")) {
  					initiallyMinimized = bool(initiallyMinimizedParam);
  					minimized = initiallyMinimized;
  				}
  			};
  
  			this.isUseDocumentWrite = function() { return useDocumentWrite; };
  			this.setUseDocumentWrite = function(useDocumentWriteParam) {
  				if (checkCanConfigure("useDocumentWrite")) {
  					useDocumentWrite = bool(useDocumentWriteParam);
  				}
  			};
  
  			// Common methods
  			function QueuedLoggingEvent(loggingEvent, formattedMessage) {
  				this.loggingEvent = loggingEvent;
  				this.levelName = loggingEvent.level.name;
  				this.formattedMessage = formattedMessage;
  			}
  
  			QueuedLoggingEvent.prototype.append = function() {
  				getConsoleWindow().log(this.levelName, this.formattedMessage);
  			};
  
  			function QueuedGroup(name, initiallyExpanded) {
  				this.name = name;
  				this.initiallyExpanded = initiallyExpanded;
  			}
  
  			QueuedGroup.prototype.append = function() {
  				getConsoleWindow().group(this.name, this.initiallyExpanded);
  			};
  
  			function QueuedGroupEnd() {}
  
  			QueuedGroupEnd.prototype.append = function() {
  				getConsoleWindow().groupEnd();
  			};
  
  			var checkAndAppend = function() {
  				// Next line forces a check of whether the window has been closed
  				safeToAppend();
  				if (!initialized) {
  					init();
  				} else if (consoleClosed && reopenWhenClosed) {
  					createWindow();
  				}
  				if (safeToAppend()) {
  					appendQueuedLoggingEvents();
  				}
  			};
  
  			this.append = function(loggingEvent) {
  				if (isSupported) {
  					// Format the message
  					var formattedMessage = appender.getLayout().formatWithException(loggingEvent);
  					queuedLoggingEvents.push(new QueuedLoggingEvent(loggingEvent, formattedMessage));
  					checkAndAppend();
  				}
  			};
  
  			this.group = function(name, initiallyExpanded) {
  				if (isSupported) {
  					queuedLoggingEvents.push(new QueuedGroup(name, initiallyExpanded));
  					checkAndAppend();
  				}
  			};
  
  			this.groupEnd = function() {
  				if (isSupported) {
  					queuedLoggingEvents.push(new QueuedGroupEnd());
  					checkAndAppend();
  				}
  			};
  
  			var appendQueuedLoggingEvents = function() {
  				while (queuedLoggingEvents.length > 0) {
  					queuedLoggingEvents.shift().append();
  				}
  				if (focusConsoleWindow) {
  					getConsoleWindow().focus();
  				}
  			};
  
  			this.setAddedToLogger = function(logger) {
  				this.loggers.push(logger);
  				if (enabled && !lazyInit) {
  					init();
  				}
  			};
  
  			this.clear = function() {
  				if (consoleWindowExists()) {
  					getConsoleWindow().clearLog();
  				}
  				queuedLoggingEvents.length = 0;
  			};
  
  			this.focus = function() {
  				if (consoleWindowExists()) {
  					getConsoleWindow().focus();
  				}
  			};
  
  			this.focusCommandLine = function() {
  				if (consoleWindowExists()) {
  					getConsoleWindow().focusCommandLine();
  				}
  			};
  
  			this.focusSearch = function() {
  				if (consoleWindowExists()) {
  					getConsoleWindow().focusSearch();
  				}
  			};
  
  			var commandWindow = window;
  
  			this.getCommandWindow = function() { return commandWindow; };
  			this.setCommandWindow = function(commandWindowParam) {
  				commandWindow = commandWindowParam;
  			};
  
  			this.executeLastCommand = function() {
  				if (consoleWindowExists()) {
  					getConsoleWindow().evalLastCommand();
  				}
  			};
  
  			var commandLayout = new PatternLayout("%m");
  			this.getCommandLayout = function() { return commandLayout; };
  			this.setCommandLayout = function(commandLayoutParam) {
  				commandLayout = commandLayoutParam;
  			};
  
  			this.evalCommandAndAppend = function(expr) {
  				var commandReturnValue = { appendResult: true, isError: false };
  				var commandOutput = "";
  				// Evaluate the command
  				try {
  					var result, i;
  					// The next three lines constitute a workaround for IE. Bizarrely, iframes seem to have no
  					// eval method on the window object initially, but once execScript has been called on
  					// it once then the eval method magically appears. See http://www.thismuchiknow.co.uk/?p=25
  					if (!commandWindow.eval && commandWindow.execScript) {
  						commandWindow.execScript("null");
  					}
  
  					var commandLineFunctionsHash = {};
  					for (i = 0, len = commandLineFunctions.length; i < len; i++) {
  						commandLineFunctionsHash[commandLineFunctions[i][0]] = commandLineFunctions[i][1];
  					}
  
  					// Keep an array of variables that are being changed in the command window so that they
  					// can be restored to their original values afterwards
  					var objectsToRestore = [];
  					var addObjectToRestore = function(name) {
  						objectsToRestore.push([name, commandWindow[name]]);
  					};
  
  					addObjectToRestore("appender");
  					commandWindow.appender = appender;
  
  					addObjectToRestore("commandReturnValue");
  					commandWindow.commandReturnValue = commandReturnValue;
  
  					addObjectToRestore("commandLineFunctionsHash");
  					commandWindow.commandLineFunctionsHash = commandLineFunctionsHash;
  
  					var addFunctionToWindow = function(name) {
  						addObjectToRestore(name);
  						commandWindow[name] = function() {
  							return this.commandLineFunctionsHash[name](appender, arguments, commandReturnValue);
  						};
  					};
  
  					for (i = 0, len = commandLineFunctions.length; i < len; i++) {
  						addFunctionToWindow(commandLineFunctions[i][0]);
  					}
  
  					// Another bizarre workaround to get IE to eval in the global scope
  					if (commandWindow === window && commandWindow.execScript) {
  						addObjectToRestore("evalExpr");
  						addObjectToRestore("result");
  						window.evalExpr = expr;
  						commandWindow.execScript("window.result=eval(window.evalExpr);");
  						result = window.result;
  					} else {
  						result = commandWindow.eval(expr);
  					}
  					commandOutput = isUndefined(result) ? result : formatObjectExpansion(result, commandLineObjectExpansionDepth);
  
  					// Restore variables in the command window to their original state
  					for (i = 0, len = objectsToRestore.length; i < len; i++) {
  						commandWindow[objectsToRestore[i][0]] = objectsToRestore[i][1];
  					}
  				} catch (ex) {
  					commandOutput = "Error evaluating command: " + getExceptionStringRep(ex);
  					commandReturnValue.isError = true;
  				}
  				// Append command output
  				if (commandReturnValue.appendResult) {
  					var message = ">>> " + expr;
  					if (!isUndefined(commandOutput)) {
  						message += newLine + commandOutput;
  					}
  					var level = commandReturnValue.isError ? Level.ERROR : Level.INFO;
  					var loggingEvent = new LoggingEvent(null, new Date(), level, [message], null);
  					var mainLayout = this.getLayout();
  					this.setLayout(commandLayout);
  					this.append(loggingEvent);
  					this.setLayout(mainLayout);
  				}
  			};
  
  			var commandLineFunctions = defaultCommandLineFunctions.concat([]);
  
  			this.addCommandLineFunction = function(functionName, commandLineFunction) {
  				commandLineFunctions.push([functionName, commandLineFunction]);
  			};
  
  			var commandHistoryCookieName = "log4javascriptCommandHistory";
  			this.storeCommandHistory = function(commandHistory) {
  				setCookie(commandHistoryCookieName, commandHistory.join(","));
  			};
  
  			var writeHtml = function(doc) {
  				var lines = getConsoleHtmlLines();
  				doc.open();
  				for (var i = 0, len = lines.length; i < len; i++) {
  					doc.writeln(lines[i]);
  				}
  				doc.close();
  			};
  
  			// Set up event listeners
  			this.setEventTypes(["load", "unload"]);
  
  			var consoleWindowLoadHandler = function() {
  				var win = getConsoleWindow();
  				win.setAppender(appender);
  				win.setNewestAtTop(newestMessageAtTop);
  				win.setScrollToLatest(scrollToLatestMessage);
  				win.setMaxMessages(maxMessages);
  				win.setShowCommandLine(showCommandLine);
  				win.setShowHideButton(showHideButton);
  				win.setShowCloseButton(showCloseButton);
  				win.setMainWindow(window);
  
  				// Restore command history stored in cookie
  				var storedValue = getCookie(commandHistoryCookieName);
  				if (storedValue) {
  					win.commandHistory = storedValue.split(",");
  					win.currentCommandIndex = win.commandHistory.length;
  				}
  
  				appender.dispatchEvent("load", { "win" : win });
  			};
  
  			this.unload = function() {
  				logLog.debug("unload " + this + ", caller: " + this.unload.caller);
  				if (!consoleClosed) {
  					logLog.debug("really doing unload " + this);
  					consoleClosed = true;
  					consoleWindowLoaded = false;
  					consoleWindowCreated = false;
  					appender.dispatchEvent("unload", {});
  				}
  			};
  
  			var pollConsoleWindow = function(windowTest, interval, successCallback, errorMessage) {
  				function doPoll() {
  					try {
  						// Test if the console has been closed while polling
  						if (consoleClosed) {
  							clearInterval(poll);
  						}
  						if (windowTest(getConsoleWindow())) {
  							clearInterval(poll);
  							successCallback();
  						}
  					} catch (ex) {
  						clearInterval(poll);
  						isSupported = false;
  						handleError(errorMessage, ex);
  					}
  				}
  
  				// Poll the pop-up since the onload event is not reliable
  				var poll = setInterval(doPoll, interval);
  			};
  
  			var getConsoleUrl = function() {
  				var documentDomainSet = (document.domain != location.hostname);
  				return useDocumentWrite ? "" : getBaseUrl() + "console_uncompressed.html" +
  											   (documentDomainSet ? "?log4javascript_domain=" + escape(document.domain) : "");
  			};
  
  			// Define methods and properties that vary between subclasses
  			if (inPage) {
  				// InPageAppender
  
  				var containerElement = null;
  
  				// Configuration methods. The function scope is used to prevent
  				// direct alteration to the appender configuration properties.
  				var cssProperties = [];
  				this.addCssProperty = function(name, value) {
  					if (checkCanConfigure("cssProperties")) {
  						cssProperties.push([name, value]);
  					}
  				};
  
  				// Define useful variables
  				var windowCreationStarted = false;
  				var iframeContainerDiv;
  				var iframeId = uniqueId + "_InPageAppender_" + consoleAppenderId;
  
  				this.hide = function() {
  					if (initialized && consoleWindowCreated) {
  						if (consoleWindowExists()) {
  							getConsoleWindow().$("command").blur();
  						}
  						iframeContainerDiv.style.display = "none";
  						minimized = true;
  					}
  				};
  
  				this.show = function() {
  					if (initialized) {
  						if (consoleWindowCreated) {
  							iframeContainerDiv.style.display = "block";
  							this.setShowCommandLine(showCommandLine); // Force IE to update
  							minimized = false;
  						} else if (!windowCreationStarted) {
  							createWindow(true);
  						}
  					}
  				};
  
  				this.isVisible = function() {
  					return !minimized && !consoleClosed;
  				};
  
  				this.close = function(fromButton) {
  					if (!consoleClosed && (!fromButton || confirm("This will permanently remove the console from the page. No more messages will be logged. Do you wish to continue?"))) {
  						iframeContainerDiv.parentNode.removeChild(iframeContainerDiv);
  						this.unload();
  					}
  				};
  
  				// Create open, init, getConsoleWindow and safeToAppend functions
  				open = function() {
  					var initErrorMessage = "InPageAppender.open: unable to create console iframe";
  
  					function finalInit() {
  						try {
  							if (!initiallyMinimized) {
  								appender.show();
  							}
  							consoleWindowLoadHandler();
  							consoleWindowLoaded = true;
  							appendQueuedLoggingEvents();
  						} catch (ex) {
  							isSupported = false;
  							handleError(initErrorMessage, ex);
  						}
  					}
  
  					function writeToDocument() {
  						try {
  							var windowTest = function(win) { return isLoaded(win); };
  							if (useDocumentWrite) {
  								writeHtml(getConsoleWindow().document);
  							}
  							if (windowTest(getConsoleWindow())) {
  								finalInit();
  							} else {
  								pollConsoleWindow(windowTest, 100, finalInit, initErrorMessage);
  							}
  						} catch (ex) {
  							isSupported = false;
  							handleError(initErrorMessage, ex);
  						}
  					}
  
  					minimized = false;
  					iframeContainerDiv = containerElement.appendChild(document.createElement("div"));
  
  					iframeContainerDiv.style.width = width;
  					iframeContainerDiv.style.height = height;
  					iframeContainerDiv.style.border = "solid gray 1px";
  
  					for (var i = 0, len = cssProperties.length; i < len; i++) {
  						iframeContainerDiv.style[cssProperties[i][0]] = cssProperties[i][1];
  					}
  
  					var iframeSrc = useDocumentWrite ? "" : " src='" + getConsoleUrl() + "'";
  
  					// Adding an iframe using the DOM would be preferable, but it doesn't work
  					// in IE5 on Windows, or in Konqueror prior to version 3.5 - in Konqueror
  					// it creates the iframe fine but I haven't been able to find a way to obtain
  					// the iframe's window object
  					iframeContainerDiv.innerHTML = "<iframe id='" + iframeId + "' name='" + iframeId +
  						"' width='100%' height='100%' frameborder='0'" + iframeSrc +
  						" scrolling='no'></iframe>";
  					consoleClosed = false;
  
  					// Write the console HTML to the iframe
  					var iframeDocumentExistsTest = function(win) {
  						try {
  							return bool(win) && bool(win.document);
  						} catch (ex) {
  							return false;
  						}
  					};
  					if (iframeDocumentExistsTest(getConsoleWindow())) {
  						writeToDocument();
  					} else {
  						pollConsoleWindow(iframeDocumentExistsTest, 100, writeToDocument, initErrorMessage);
  					}
  					consoleWindowCreated = true;
  				};
  
  				createWindow = function(show) {
  					if (show || !initiallyMinimized) {
  						var pageLoadHandler = function() {
  							if (!container) {
  								// Set up default container element
  								containerElement = document.createElement("div");
  								containerElement.style.position = "fixed";
  								containerElement.style.left = "0";
  								containerElement.style.right = "0";
  								containerElement.style.bottom = "0";
  								document.body.appendChild(containerElement);
  								appender.addCssProperty("borderWidth", "1px 0 0 0");
  								appender.addCssProperty("zIndex", 1000000); // Can't find anything authoritative that says how big z-index can be
  								open();
  							} else {
  								try {
  									var el = document.getElementById(container);
  									if (el.nodeType == 1) {
  										containerElement = el;
  									}
  									open();
  								} catch (ex) {
  									handleError("InPageAppender.init: invalid container element '" + container + "' supplied", ex);
  								}
  							}
  						};
  
  						// Test the type of the container supplied. First, check if it's an element
  						if (pageLoaded && container && container.appendChild) {
  							containerElement = container;
  							open();
  						} else if (pageLoaded) {
  							pageLoadHandler();
  						} else {
  							log4javascript.addEventListener("load", pageLoadHandler);
  						}
  						windowCreationStarted = true;
  					}
  				};
  
  				init = function() {
  					createWindow();
  					initialized = true;
  				};
  
  				getConsoleWindow = function() {
  					var iframe = window.frames[iframeId];
  					if (iframe) {
  						return iframe;
  					}
  				};
  
  				safeToAppend = function() {
  					if (isSupported && !consoleClosed) {
  						if (consoleWindowCreated && !consoleWindowLoaded && getConsoleWindow() && isLoaded(getConsoleWindow())) {
  							consoleWindowLoaded = true;
  						}
  						return consoleWindowLoaded;
  					}
  					return false;
  				};
  			} else {
  				// PopUpAppender
  
  				// Extract params
  				var useOldPopUp = appender.defaults.useOldPopUp;
  				var complainAboutPopUpBlocking = appender.defaults.complainAboutPopUpBlocking;
  				var reopenWhenClosed = this.defaults.reopenWhenClosed;
  
  				// Configuration methods. The function scope is used to prevent
  				// direct alteration to the appender configuration properties.
  				this.isUseOldPopUp = function() { return useOldPopUp; };
  				this.setUseOldPopUp = function(useOldPopUpParam) {
  					if (checkCanConfigure("useOldPopUp")) {
  						useOldPopUp = bool(useOldPopUpParam);
  					}
  				};
  
  				this.isComplainAboutPopUpBlocking = function() { return complainAboutPopUpBlocking; };
  				this.setComplainAboutPopUpBlocking = function(complainAboutPopUpBlockingParam) {
  					if (checkCanConfigure("complainAboutPopUpBlocking")) {
  						complainAboutPopUpBlocking = bool(complainAboutPopUpBlockingParam);
  					}
  				};
  
  				this.isFocusPopUp = function() { return focusConsoleWindow; };
  				this.setFocusPopUp = function(focusPopUpParam) {
  					// This property can be safely altered after logging has started
  					focusConsoleWindow = bool(focusPopUpParam);
  				};
  
  				this.isReopenWhenClosed = function() { return reopenWhenClosed; };
  				this.setReopenWhenClosed = function(reopenWhenClosedParam) {
  					// This property can be safely altered after logging has started
  					reopenWhenClosed = bool(reopenWhenClosedParam);
  				};
  
  				this.close = function() {
  					logLog.debug("close " + this);
  					try {
  						popUp.close();
  						this.unload();
  					} catch (ex) {
  						// Do nothing
  					}
  				};
  
  				this.hide = function() {
  					logLog.debug("hide " + this);
  					if (consoleWindowExists()) {
  						this.close();
  					}
  				};
  
  				this.show = function() {
  					logLog.debug("show " + this);
  					if (!consoleWindowCreated) {
  						open();
  					}
  				};
  
  				this.isVisible = function() {
  					return safeToAppend();
  				};
  
  				// Define useful variables
  				var popUp;
  
  				// Create open, init, getConsoleWindow and safeToAppend functions
  				open = function() {
  					var windowProperties = "width=" + width + ",height=" + height + ",status,resizable";
  					var frameInfo = "";
  					try {
  						var frameEl = window.frameElement;
  						if (frameEl) {
  							frameInfo = "_" + frameEl.tagName + "_" + (frameEl.name || frameEl.id || "");
  						}
  					} catch (e) {
  						frameInfo = "_inaccessibleParentFrame";
  					}
  					var windowName = "PopUp_" + location.host.replace(/[^a-z0-9]/gi, "_") + "_" + consoleAppenderId + frameInfo;
  					if (!useOldPopUp || !useDocumentWrite) {
  						// Ensure a previous window isn't used by using a unique name
  						windowName = windowName + "_" + uniqueId;
  					}
  
  					var checkPopUpClosed = function(win) {
  						if (consoleClosed) {
  							return true;
  						} else {
  							try {
  								return bool(win) && win.closed;
  							} catch(ex) {}
  						}
  						return false;
  					};
  
  					var popUpClosedCallback = function() {
  						if (!consoleClosed) {
  							appender.unload();
  						}
  					};
  
  					function finalInit() {
  						getConsoleWindow().setCloseIfOpenerCloses(!useOldPopUp || !useDocumentWrite);
  						consoleWindowLoadHandler();
  						consoleWindowLoaded = true;
  						appendQueuedLoggingEvents();
  						pollConsoleWindow(checkPopUpClosed, 500, popUpClosedCallback,
  								"PopUpAppender.checkPopUpClosed: error checking pop-up window");
  					}
  
  					try {
  						popUp = window.open(getConsoleUrl(), windowName, windowProperties);
  						consoleClosed = false;
  						consoleWindowCreated = true;
  						if (popUp && popUp.document) {
  							if (useDocumentWrite && useOldPopUp && isLoaded(popUp)) {
  								popUp.mainPageReloaded();
  								finalInit();
  							} else {
  								if (useDocumentWrite) {
  									writeHtml(popUp.document);
  								}
  								// Check if the pop-up window object is available
  								var popUpLoadedTest = function(win) { return bool(win) && isLoaded(win); };
  								if (isLoaded(popUp)) {
  									finalInit();
  								} else {
  									pollConsoleWindow(popUpLoadedTest, 100, finalInit,
  											"PopUpAppender.init: unable to create console window");
  								}
  							}
  						} else {
  							isSupported = false;
  							logLog.warn("PopUpAppender.init: pop-ups blocked, please unblock to use PopUpAppender");
  							if (complainAboutPopUpBlocking) {
  								handleError("log4javascript: pop-up windows appear to be blocked. Please unblock them to use pop-up logging.");
  							}
  						}
  					} catch (ex) {
  						handleError("PopUpAppender.init: error creating pop-up", ex);
  					}
  				};
  
  				createWindow = function() {
  					if (!initiallyMinimized) {
  						open();
  					}
  				};
  
  				init = function() {
  					createWindow();
  					initialized = true;
  				};
  
  				getConsoleWindow = function() {
  					return popUp;
  				};
  
  				safeToAppend = function() {
  					if (isSupported && !isUndefined(popUp) && !consoleClosed) {
  						if (popUp.closed ||
  								(consoleWindowLoaded && isUndefined(popUp.closed))) { // Extra check for Opera
  							appender.unload();
  							logLog.debug("PopUpAppender: pop-up closed");
  							return false;
  						}
  						if (!consoleWindowLoaded && isLoaded(popUp)) {
  							consoleWindowLoaded = true;
  						}
  					}
  					return isSupported && consoleWindowLoaded && !consoleClosed;
  				};
  			}
  
  			// Expose getConsoleWindow so that automated tests can check the DOM
  			this.getConsoleWindow = getConsoleWindow;
  		};
  
  		ConsoleAppender.addGlobalCommandLineFunction = function(functionName, commandLineFunction) {
  			defaultCommandLineFunctions.push([functionName, commandLineFunction]);
  		};
  
  		/* ------------------------------------------------------------------ */
  
  		function PopUpAppender(lazyInit, initiallyMinimized, useDocumentWrite,
  							   width, height) {
  			this.create(false, null, lazyInit, initiallyMinimized,
  					useDocumentWrite, width, height, this.defaults.focusPopUp);
  		}
  
  		PopUpAppender.prototype = new ConsoleAppender();
  
  		PopUpAppender.prototype.defaults = {
  			layout: new PatternLayout("%d{HH:mm:ss} %-5p - %m{1}%n"),
  			initiallyMinimized: false,
  			focusPopUp: false,
  			lazyInit: true,
  			useOldPopUp: true,
  			complainAboutPopUpBlocking: true,
  			newestMessageAtTop: false,
  			scrollToLatestMessage: true,
  			width: "600",
  			height: "400",
  			reopenWhenClosed: false,
  			maxMessages: null,
  			showCommandLine: true,
  			commandLineObjectExpansionDepth: 1,
  			showHideButton: false,
  			showCloseButton: true,
  			useDocumentWrite: true
  		};
  
  		PopUpAppender.prototype.toString = function() {
  			return "PopUpAppender";
  		};
  
  		log4javascript.PopUpAppender = PopUpAppender;
  
  		/* ------------------------------------------------------------------ */
  
  		function InPageAppender(container, lazyInit, initiallyMinimized,
  								useDocumentWrite, width, height) {
  			this.create(true, container, lazyInit, initiallyMinimized,
  					useDocumentWrite, width, height, false);
  		}
  
  		InPageAppender.prototype = new ConsoleAppender();
  
  		InPageAppender.prototype.defaults = {
  			layout: new PatternLayout("%d{HH:mm:ss} %-5p - %m{1}%n"),
  			initiallyMinimized: false,
  			lazyInit: true,
  			newestMessageAtTop: false,
  			scrollToLatestMessage: true,
  			width: "100%",
  			height: "220px",
  			maxMessages: null,
  			showCommandLine: true,
  			commandLineObjectExpansionDepth: 1,
  			showHideButton: false,
  			showCloseButton: false,
  			showLogEntryDeleteButtons: true,
  			useDocumentWrite: true
  		};
  
  		InPageAppender.prototype.toString = function() {
  			return "InPageAppender";
  		};
  
  		log4javascript.InPageAppender = InPageAppender;
  
  		// Next line for backwards compatibility
  		log4javascript.InlineAppender = InPageAppender;
  	})();
  	/* ---------------------------------------------------------------------- */
  	// Console extension functions
  
  	function padWithSpaces(str, len) {
  		if (str.length < len) {
  			var spaces = [];
  			var numberOfSpaces = Math.max(0, len - str.length);
  			for (var i = 0; i < numberOfSpaces; i++) {
  				spaces[i] = " ";
  			}
  			str += spaces.join("");
  		}
  		return str;
  	}
  
  	(function() {
  		function dir(obj) {
  			var maxLen = 0;
  			// Obtain the length of the longest property name
  			for (var p in obj) {
  				maxLen = Math.max(toStr(p).length, maxLen);
  			}
  			// Create the nicely formatted property list
  			var propList = [];
  			for (p in obj) {
  				var propNameStr = "  " + padWithSpaces(toStr(p), maxLen + 2);
  				var propVal;
  				try {
  					propVal = splitIntoLines(toStr(obj[p])).join(padWithSpaces(newLine, maxLen + 6));
  				} catch (ex) {
  					propVal = "[Error obtaining property. Details: " + getExceptionMessage(ex) + "]";
  				}
  				propList.push(propNameStr + propVal);
  			}
  			return propList.join(newLine);
  		}
  
  		var nodeTypes = {
  			ELEMENT_NODE: 1,
  			ATTRIBUTE_NODE: 2,
  			TEXT_NODE: 3,
  			CDATA_SECTION_NODE: 4,
  			ENTITY_REFERENCE_NODE: 5,
  			ENTITY_NODE: 6,
  			PROCESSING_INSTRUCTION_NODE: 7,
  			COMMENT_NODE: 8,
  			DOCUMENT_NODE: 9,
  			DOCUMENT_TYPE_NODE: 10,
  			DOCUMENT_FRAGMENT_NODE: 11,
  			NOTATION_NODE: 12
  		};
  
  		var preFormattedElements = ["script", "pre"];
  
  		// This should be the definitive list, as specified by the XHTML 1.0 Transitional DTD
  		var emptyElements = ["br", "img", "hr", "param", "link", "area", "input", "col", "base", "meta"];
  		var indentationUnit = "  ";
  
  		// Create and return an XHTML string from the node specified
  		function getXhtml(rootNode, includeRootNode, indentation, startNewLine, preformatted) {
  			includeRootNode = (typeof includeRootNode == "undefined") ? true : !!includeRootNode;
  			if (typeof indentation != "string") {
  				indentation = "";
  			}
  			startNewLine = !!startNewLine;
  			preformatted = !!preformatted;
  			var xhtml;
  
  			function isWhitespace(node) {
  				return ((node.nodeType == nodeTypes.TEXT_NODE) && /^[ \t\r\n]*$/.test(node.nodeValue));
  			}
  
  			function fixAttributeValue(attrValue) {
  				return attrValue.toString().replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
  			}
  
  			function getStyleAttributeValue(el) {
  				var stylePairs = el.style.cssText.split(";");
  				var styleValue = "";
  				for (var j = 0, len = stylePairs.length; j < len; j++) {
  					var nameValueBits = stylePairs[j].split(":");
  					var props = [];
  					if (!/^\s*$/.test(nameValueBits[0])) {
  						props.push(trim(nameValueBits[0]).toLowerCase() + ":" + trim(nameValueBits[1]));
  					}
  					styleValue = props.join(";");
  				}
  				return styleValue;
  			}
  
  			function getNamespace(el) {
  				if (el.prefix) {
  					return el.prefix;
  				} else if (el.outerHTML) {
  					var regex = new RegExp("<([^:]+):" + el.tagName + "[^>]*>", "i");
  					if (regex.test(el.outerHTML)) {
  						return RegExp.$1.toLowerCase();
  					}
  				}
                  return "";
  			}
  
  			var lt = "<";
  			var gt = ">";
  			var i, len;
  
  			if (includeRootNode && rootNode.nodeType != nodeTypes.DOCUMENT_FRAGMENT_NODE) {
  				switch (rootNode.nodeType) {
  					case nodeTypes.ELEMENT_NODE:
  						var tagName = rootNode.tagName.toLowerCase();
  						xhtml = startNewLine ? newLine + indentation : "";
  						xhtml += lt;
  						// Allow for namespaces, where present
  						var prefix = getNamespace(rootNode);
  						var hasPrefix = !!prefix;
  						if (hasPrefix) {
  							xhtml += prefix + ":";
  						}
  						xhtml += tagName;
  						for (i = 0, len = rootNode.attributes.length; i < len; i++) {
  							var currentAttr = rootNode.attributes[i];
  							// Check the attribute is valid.
  							if (!	currentAttr.specified ||
  									currentAttr.nodeValue === null ||
  									currentAttr.nodeName.toLowerCase() === "style" ||
  									typeof currentAttr.nodeValue !== "string" ||
  									currentAttr.nodeName.indexOf("_moz") === 0) {
  								continue;
  							}
  							xhtml += " " + currentAttr.nodeName.toLowerCase() + "=\"";
  							xhtml += fixAttributeValue(currentAttr.nodeValue);
  							xhtml += "\"";
  						}
  						// Style needs to be done separately as it is not reported as an
  						// attribute in IE
  						if (rootNode.style.cssText) {
  							var styleValue = getStyleAttributeValue(rootNode);
  							if (styleValue !== "") {
  								xhtml += " style=\"" + getStyleAttributeValue(rootNode) + "\"";
  							}
  						}
  						if (array_contains(emptyElements, tagName) ||
  								(hasPrefix && !rootNode.hasChildNodes())) {
  							xhtml += "/" + gt;
  						} else {
  							xhtml += gt;
  							// Add output for childNodes collection (which doesn't include attribute nodes)
  							var childStartNewLine = !(rootNode.childNodes.length === 1 &&
  								rootNode.childNodes[0].nodeType === nodeTypes.TEXT_NODE);
  							var childPreformatted = array_contains(preFormattedElements, tagName);
  							for (i = 0, len = rootNode.childNodes.length; i < len; i++) {
  								xhtml += getXhtml(rootNode.childNodes[i], true, indentation + indentationUnit,
  									childStartNewLine, childPreformatted);
  							}
  							// Add the end tag
  							var endTag = lt + "/" + tagName + gt;
  							xhtml += childStartNewLine ? newLine + indentation + endTag : endTag;
  						}
  						return xhtml;
  					case nodeTypes.TEXT_NODE:
  						if (isWhitespace(rootNode)) {
  							xhtml = "";
  						} else {
  							if (preformatted) {
  								xhtml = rootNode.nodeValue;
  							} else {
  								// Trim whitespace from each line of the text node
  								var lines = splitIntoLines(trim(rootNode.nodeValue));
  								var trimmedLines = [];
  								for (i = 0, len = lines.length; i < len; i++) {
  									trimmedLines[i] = trim(lines[i]);
  								}
  								xhtml = trimmedLines.join(newLine + indentation);
  							}
  							if (startNewLine) {
  								xhtml = newLine + indentation + xhtml;
  							}
  						}
  						return xhtml;
  					case nodeTypes.CDATA_SECTION_NODE:
  						return "<![CDA" + "TA[" + rootNode.nodeValue + "]" + "]>" + newLine;
  					case nodeTypes.DOCUMENT_NODE:
  						xhtml = "";
  						// Add output for childNodes collection (which doesn't include attribute nodes)
  						for (i = 0, len = rootNode.childNodes.length; i < len; i++) {
  							xhtml += getXhtml(rootNode.childNodes[i], true, indentation);
  						}
  						return xhtml;
  					default:
  						return "";
  				}
  			} else {
  				xhtml = "";
  				// Add output for childNodes collection (which doesn't include attribute nodes)
  				for (i = 0, len = rootNode.childNodes.length; i < len; i++) {
  					xhtml += getXhtml(rootNode.childNodes[i], true, indentation + indentationUnit);
  				}
  				return xhtml;
  			}
  		}
  
  		function createCommandLineFunctions() {
  			ConsoleAppender.addGlobalCommandLineFunction("$", function(appender, args, returnValue) {
  				return document.getElementById(args[0]);
  			});
  
  			ConsoleAppender.addGlobalCommandLineFunction("dir", function(appender, args, returnValue) {
  				var lines = [];
  				for (var i = 0, len = args.length; i < len; i++) {
  					lines[i] = dir(args[i]);
  				}
  				return lines.join(newLine + newLine);
  			});
  
  			ConsoleAppender.addGlobalCommandLineFunction("dirxml", function(appender, args, returnValue) {
  				var lines = [];
  				for (var i = 0, len = args.length; i < len; i++) {
  					lines[i] = getXhtml(args[i]);
  				}
  				return lines.join(newLine + newLine);
  			});
  
  			ConsoleAppender.addGlobalCommandLineFunction("cd", function(appender, args, returnValue) {
  				var win, message;
  				if (args.length === 0 || args[0] === "") {
  					win = window;
  					message = "Command line set to run in main window";
  				} else {
  					if (args[0].window == args[0]) {
  						win = args[0];
  						message = "Command line set to run in frame '" + args[0].name + "'";
  					} else {
  						win = window.frames[args[0]];
  						if (win) {
  							message = "Command line set to run in frame '" + args[0] + "'";
  						} else {
  							returnValue.isError = true;
  							message = "Frame '" + args[0] + "' does not exist";
  							win = appender.getCommandWindow();
  						}
  					}
  				}
  				appender.setCommandWindow(win);
  				return message;
  			});
  
  			ConsoleAppender.addGlobalCommandLineFunction("clear", function(appender, args, returnValue) {
  				returnValue.appendResult = false;
  				appender.clear();
  			});
  
  			ConsoleAppender.addGlobalCommandLineFunction("keys", function(appender, args, returnValue) {
  				var keys = [];
  				for (var k in args[0]) {
  					keys.push(k);
  				}
  				return keys;
  			});
  
  			ConsoleAppender.addGlobalCommandLineFunction("values", function(appender, args, returnValue) {
  				var values = [];
  				for (var k in args[0]) {
  					try {
  						values.push(args[0][k]);
  					} catch (ex) {
  						logLog.warn("values(): Unable to obtain value for key " + k + ". Details: " + getExceptionMessage(ex));
  					}
  				}
  				return values;
  			});
  
  			ConsoleAppender.addGlobalCommandLineFunction("expansionDepth", function(appender, args, returnValue) {
  				var expansionDepth = parseInt(args[0], 10);
  				if (isNaN(expansionDepth) || expansionDepth < 0) {
  					returnValue.isError = true;
  					return "" + args[0] + " is not a valid expansion depth";
  				} else {
  					appender.setCommandLineObjectExpansionDepth(expansionDepth);
  					return "Object expansion depth set to " + expansionDepth;
  				}
  			});
  		}
  
  		function init() {
  			// Add command line functions
  			createCommandLineFunctions();
  		}
  
  		/* ------------------------------------------------------------------ */
  
  		init();
  	})();
  
  	/* ---------------------------------------------------------------------- */
  
  	function createDefaultLogger() {
  		var logger = log4javascript.getLogger(defaultLoggerName);
  		var a = new log4javascript.PopUpAppender();
  		logger.addAppender(a);
  		return logger;
  	}
  
  	/* ---------------------------------------------------------------------- */
  	// Main load
  
  	log4javascript.setDocumentReady = function() {
  		pageLoaded = true;
  		log4javascript.dispatchEvent("load", {});
  	};
  
  	if (window.addEventListener) {
  		window.addEventListener("load", log4javascript.setDocumentReady, false);
  	} else if (window.attachEvent) {
  		window.attachEvent("onload", log4javascript.setDocumentReady);
  	} else {
  		var oldOnload = window.onload;
  		if (typeof window.onload != "function") {
  			window.onload = log4javascript.setDocumentReady;
  		} else {
  			window.onload = function(evt) {
  				if (oldOnload) {
  					oldOnload(evt);
  				}
  				log4javascript.setDocumentReady();
  			};
  		}
  	}
  
  	return log4javascript;
  }, this);