/**
 * Copyright 2015 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.13
 * Edition: log4javascript
 * Build date: 23 May 2015
 * Website: http://log4javascript.org
 */

(function (factory, root) {
    if (typeof define == "function" && define.amd) {
        // 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.13";
    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 (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 (concatenate) {
            var formattedMessage = appender.getLayout().formatWithException(loggingEvent);
            return (typeof formattedMessage == "string") ?
                (concatenate ? formattedMessage : [formattedMessage]) :
                (concatenate ? formattedMessage.join(" ") : formattedMessage);
        };

        var console = window.console;

        if (console && console.log) {
            // 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 (typeof console[consoleMethodName].apply == "function") {
                console[consoleMethodName].apply(console, getFormattedMessage(false));
            } else {
                console[consoleMethodName](getFormattedMessage(true));
            }
        } else if ((typeof opera != "undefined") && opera.postError) { // Opera
            opera.postError(getFormattedMessage(true));
        }
    };

    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);