/** * @module Debug * @namespace springroll */ (function() { // Import classes var Enum = include('springroll.Enum'), slice = Array.prototype.slice; /** * A static closure to provide easy access to the console * without having errors if the console doesn't exist * to use call: Debug.log('Your log here') * * @class Debug * @static */ var Debug = {}; /** * If we have a console * * @private * @property {Boolean} _hasConsole */ var _hasConsole = (window.console !== undefined); /** * If the console supports coloring * * @private * @property {Boolean} _consoleSupportsColors */ //document.documentMode is an IE only property specifying what version of IE the document is //being displayed for var _consoleSupportsColors = document.documentMode === undefined; // Because of the compile constants, we need to // cut this word into pieces and do a dynamic access var DEBUGKEY = 'DE' + 'BUG'; if (_hasConsole) { try { // detect IE9's issue with apply on console functions console.assert.apply(console, [true, "IE9 test"]); } catch (error) { // Reference to the bind method var bind = Function.prototype.bind; // Bind all these methods in order to use apply // this is ONLY needed for IE9 var methods = [ 'log', 'debug', 'warn', 'info', 'error', 'assert', 'dir', 'trace', 'group', 'groupCollapsed', 'groupEnd' ]; // Loop through console methods for (var method, i = 0; i < methods.length; i++) { method = methods[i]; if (console[method]) { console[method] = bind.call(console[method], console); } } } } /** * The levels of logging * @property {springroll.Enum} Levels * @static */ var Levels = Debug.Levels = new Enum( /** * The most basic general log level * @property {int} Levels.GENERAL * @static */ 'GENERAL', /** * The debug log level, more priority than GENERAL * @property {int} Levels.DEBUG * @static */ DEBUGKEY, /** * The info log level, more priority than DEBUG * @property {int} Levels.DEBUG * @static */ 'INFO', /** * The warn log level, more priority than WARN * @property {int} Levels.WARN * @static */ 'WARN', /** * The error log level, the most priority log level * @property {int} Levels.ERROR * @static */ 'ERROR' ); /** * The minimum log level to show, by default it's set to * show all levels of logging. * @public * @static * @property {int} minLogLevel */ Debug.minLogLevel = Levels.GENERAL; /** * Boolean to turn on or off the debugging * @public * @static * @property {Boolean} enabled */ Debug.enabled = true; /** * The DOM element to output debug messages to * * @public * @static * @property {DOMElement} output */ Debug.output = null; /** * Browser port for the websocket - browsers tend to block lower ports * @static * @private * @property {int} NET_PORT * @default 1026 */ var NET_PORT = 1026; /** * If the WebSocket is connected * @static * @private * @default false * @property {Boolean} _useSocket */ var _useSocket = false; /** * The socket connection * @static * @private * @property {WebSocket} _socket */ var _socket = null; /** * The current message object being sent to the `WebSocket` * @static * @private * @property {Object} _socketMessage */ var _socketMessage = null; /** * The `WebSocket` message queue * @static * @private * @property {Array} _socketQueue */ var _socketQueue = null; /* * Prevents uglify from mangling function names attached to it so we can strip * out of a stack trace for logging purpose. */ var manglePeventer = {}; /** * Methods names to use to strip out lines from stack traces * in remote logging. * @static * @private * @property {Array} methodsToStrip */ var methodsToStrip = [ //general logging 'log', 'debug', 'warn', 'info', 'error', 'assert', 'dir', 'trace', 'group', 'groupCollapsed', 'groupEnd', //remote logging '_remoteLog', 'globalErrorHandler', //our color functions 'navy', 'blue', 'aqua', 'teal', 'olive', 'green', 'lime', 'yellow', 'orange', 'red', 'pink', 'purple', 'maroon', 'silver', 'gray' ]; /** * Regular expression to get the line number and column from a stack trace line. * @static * @private * @property {RegEx} lineLocationFinder */ var lineLocationFinder = /(:\d+)+/; /** * Connect to the `WebSocket` * @public * @static * @method connect * @param {String} host The remote address to connect to, IP address or host name * @return {Boolean} If a connection was attempted */ Debug.connect = function(host) { //Make sure WebSocket exists without prefixes for us if (!('WebSocket' in window) && !('MozWebSocket' in window)) return false; window.WebSocket = WebSocket || MozWebSocket; try { _socket = new WebSocket('ws://' + host + ':' + NET_PORT); _socket.onopen = onConnect; _socket.onclose = onClose; _socket.onerror = onClose; _socketQueue = []; _useSocket = true; } catch (error) { return false; } return true; }; /** * Disconnect from the `WebSocket` * @public * @static * @method disconnect */ Debug.disconnect = function() { if (_useSocket) { _socket.close(); onClose(); } }; /** * Callback when the `WebSocket` is connected * @private * @static * @method onConnect */ var onConnect = function() { //set up a function to handle all messages window.onerror = manglePeventer.globalErrorHandler; //create and send a new session message _socketMessage = { level: 'session', message: '', stack: null, time: 0 }; _socket.send(JSON.stringify(_socketMessage)); //send any queued logs for (var i = 0, len = _socketQueue.length; i < len; ++i) { _socket.send(JSON.stringify(_socketQueue[i])); } //get rid of this, since we are connected _socketQueue = null; }; /** * Global window error handler, used for remote connections. * @static * @private * @method globalErrorHandler * @param {String} message The error message * @param {String} file The url of the file * @param {int} line The line within the file * @param {int} column The column within the line * @param {Error} error The error itself */ manglePeventer.globalErrorHandler = function(message, file, line, column, error) { Debug._remoteLog(message, Levels.ERROR, error ? error.stack : null); //let the error do the normal behavior return false; }; /** * Callback for when the websocket is closed * @private * @static * @method onClose */ var onClose = function() { window.onerror = null; _useSocket = false; _socket.onopen = null; _socket.onmessage = null; _socket.onclose = null; _socket.onerror = null; _socket = null; _socketMessage = null; _socketQueue = null; }; /** * Sent to the output * @private * @static * @method domOutput * @param {String} level The log level * @param {String} args Additional arguments */ var domOutput = function(level, args) { if (Debug.output) { Debug.output.innerHTML += '<div class="' + level + '">' + args + '</div>'; } }; /** * Send a remote log message using the socket connection * @private * @static * @method _remoteLog * @param {Array} message The message to send * @param {level} [level=0] The log level to send * @param {String} [stack] A stack to use for the message. A stack will be created if stack * is omitted. * @return {Debug} The instance of debug for chaining */ Debug._remoteLog = function(message, level, stack) { level = level || Levels.GENERAL; if (!Array.isArray(message)) message = [message]; message = slice.call(message); var i, length; // Go through each argument and replace any circular // references with simplified objects for (i = 0, length = message.length; i < length; i++) { if (typeof message[i] == "object") { try { message[i] = removeCircular(message[i], 3); } catch (e) { message[i] = String(message[i]); } /*console.log(message[i]);*/ } } //figure out the stack if (!stack) stack = new Error().stack; //split stack lines stack = stack ? stack.split("\n") : []; //go through lines, figuring out what to strip out //and standardizing the format for the rest var splitIndex, functionSection, file, lineLocation, functionName, lineSearch, lastToStrip = -1, shouldStrip = true; for (i = 0, length = stack.length; i < length; ++i) { var line = stack[i].trim(); //FF has an empty string at the end if (!line) { if (i == length - 1) { stack.pop(); break; } else continue; } //strip out any actual errors in the stack trace, since that is the message //also the 'error' line from our new Error(). if (line == "Error" || line.indexOf("Error:") > -1) { lastToStrip = i; continue; } // FF/Safari style: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Stack if (line.indexOf("@") > -1) { splitIndex = line.indexOf("@"); functionSection = line.substring(0, splitIndex); //if we should strip this line out of the stack, we should stop parsing the stack //early if (functionSection.indexOf(".") != -1) functionName = functionSection.substring(functionSection.lastIndexOf(".") + 1); else functionName = functionSection; if (shouldStrip && methodsToStrip.indexOf(functionName) != -1) { lastToStrip = i; continue; } //get the file and line number/column file = line.substring(splitIndex + 1); } // Chrome/IE/Opera style: //https://msdn.microsoft.com/en-us/library/windows/apps/hh699850.aspx else { splitIndex = line.indexOf("("); //skip the "at " at the beginning of the line and the space at the end functionSection = line.substring(3, splitIndex - 1); //if we should strip this line out of the stack, we should stop parsing the stack //early if (functionSection.indexOf(".") != -1) functionName = functionSection.substring(functionSection.lastIndexOf(".") + 1); else functionName = functionSection; if (shouldStrip && methodsToStrip.indexOf(functionName) != -1) { lastToStrip = i; continue; } //get the file and line number/column, dropping the trailing ')' file = line.substring(splitIndex + 1, line.length - 2); } //find the line number/column in the combined file string lineSearch = lineLocationFinder.exec(file); //handle browsers not providing proper information (like iOS) if (!lineSearch) { stack[i] = { "function": "", "file": "", lineLocation: "" }; continue; } //split the file and line number/column from each other file = file.substring(0, lineSearch.index); lineLocation = lineSearch[0].substring(1); //If we got here, we got out of the Debug functions and should stop trying to //strip stuff out, in case someone else's functions are named the same shouldStrip = false; stack[i] = { "function": functionSection || "<anonymous>", file: file, lineLocation: lineLocation }; } if (lastToStrip >= 0) { stack = stack.slice(lastToStrip + 1); } // If we are still in the process of connecting, queue up the log if (_socketQueue) { _socketQueue.push( { message: message, level: level.name, stack: stack, time: Date.now() }); } else // send the log immediately { _socketMessage.level = level.name; _socketMessage.message = message; _socketMessage.stack = stack; _socketMessage.time = Date.now(); var send; try { send = JSON.stringify(_socketMessage); } catch (e) { _socketMessage.message = ["[circular object]"]; send = JSON.stringify(_socketMessage); } _socket.send(send); } return Debug; }; /** * An array for preventing circular references * @static * @private * @property {Array} circularArray */ var circularArray = []; /** * Strip out known circular references * @method removeCircular * @private * @param {Object} obj The object to remove references from */ var removeCircular = function(obj, maxDepth, depth) { if (Array.isArray(obj)) return obj; depth = depth || 0; if (depth === 0) circularArray.length = 0; circularArray.push(obj); var result = {}; for (var key in obj) { var value = obj[key]; // avoid doing properties that are known to be DOM objects, // because those have circular references if (value instanceof Window || value instanceof Document || value instanceof HTMLElement || key == "document" || key == "window" || key == "ownerDocument" || key == "view" || key == "target" || key == "currentTarget" || key == "originalTarget" || key == "explicitOriginalTarget" || key == "rangeParent" || key == "srcElement" || key == "relatedTarget" || key == "fromElement" || key == "toElement") { if (value instanceof HTMLElement) { var elementString; elementString = "<" + value.tagName; if (value.id) elementString += " id='" + value.id + "'"; if (value.className) elementString += " class='" + value.className + "'"; result[key] = elementString + " />"; } continue; } switch (typeof value) { case "object": { result[key] = (depth > maxDepth || circularArray.indexOf(value) > -1) ? String(value) : removeCircular(value, maxDepth, depth + 1); break; } case "function": { result[key] = "[function]"; break; } case "string": case "number": case "boolean": case "bool": { result[key] = value; break; } default: { result[key] = value; break; } } } return result; }; /** * Log something in the console or remote * @static * @public * @method log * @param {*} params The statement or object to log * @return {Debug} The instance of debug for chaining */ Debug.log = function(params) { if (!Debug.enabled) return Debug; if (_useSocket) { Debug._remoteLog(Array.prototype.slice.call(arguments)); } else if (Debug.minLogLevel == Levels.GENERAL) { if (_hasConsole) { if (arguments.length === 1) console.log(params); else console.log.apply(console, arguments); } domOutput('general', params); } return Debug; }; /** * Debug something in the console or remote * @static * @public * @method debug * @param {*} params The statement or object to debug * @return {Debug} The instance of debug for chaining */ Debug.debug = function(params) { if (!Debug.enabled) return Debug; if (_useSocket) { Debug._remoteLog(Array.prototype.slice.call(arguments), Levels[DEBUGKEY]); } else if (Debug.minLogLevel.asInt <= Levels[DEBUGKEY].asInt) { // debug() is officially deprecated if (_hasConsole) { if (console.debug) { if (arguments.length === 1) console.debug(params); else console.debug.apply(console, arguments); } else { if (arguments.length === 1) console.log(params); else console.log.apply(console, arguments); } } domOutput('debug', params); } return Debug; }; /** * Info something in the console or remote * @static * @public * @method info * @param {*} params The statement or object to info * @return {Debug} The instance of debug for chaining */ Debug.info = function(params) { if (!Debug.enabled) return Debug; if (_useSocket) { Debug._remoteLog(Array.prototype.slice.call(arguments), Levels.INFO); } else if (Debug.minLogLevel.asInt <= Levels.INFO.asInt) { if (_hasConsole) { if (arguments.length === 1) console.info(params); else console.info.apply(console, arguments); } domOutput('info', params); } return Debug; }; /** * Warn something in the console or remote * @static * @public * @method warn * @param {*} params The statement or object to warn * @return {Debug} The instance of debug for chaining */ Debug.warn = function(params) { if (!Debug.enabled) return Debug; if (_useSocket) { Debug._remoteLog(Array.prototype.slice.call(arguments), Levels.WARN); } else if (Debug.minLogLevel.asInt <= Levels.WARN.asInt) { if (_hasConsole) { if (arguments.length === 1) console.warn(params); else console.warn.apply(console, arguments); } domOutput('warn', params); } return Debug; }; /** * Error something in the console or remote * @static * @public * @method error * @param {*} params The statement or object to error */ Debug.error = function(params) { if (!Debug.enabled) return; if (_useSocket) { Debug._remoteLog(Array.prototype.slice.call(arguments), Levels.ERROR); } else { if (_hasConsole) { if (arguments.length === 1) console.error(params); else console.error.apply(console, arguments); } domOutput('error', params); } return Debug; }; /** * Assert that something is true * @static * @public * @method assert * @param {Boolean} truth As statement that is assumed true * @param {*} params The message to error if the assert is false * @return {Debug} The instance of debug for chaining */ Debug.assert = function(truth, params) { if (Debug.enabled) { if (!truth) { domOutput('error', params); if (_useSocket) { Debug._remoteLog(params, Levels.ERROR); } } if (_hasConsole && console.assert) console.assert(truth, params); } return Debug; }; /** * Method to describe an object in the console * @static * @method dir * @public * @param {Object} params The object to describe in the console * @return {Debug} The instance of debug for chaining */ Debug.dir = function(params) { if (Debug.enabled) { if (_useSocket) { Debug._remoteLog(Array.prototype.slice.call(arguments), Levels.GENERAL); } else if (_hasConsole) { if (arguments.length === 1) console.dir(params); else console.dir.apply(console, arguments); } } return Debug; }; /** * Method to clear the console * @static * @public * @method clear * @return {Debug} The instance of debug for chaining */ Debug.clear = function() { if (Debug.enabled) { if (_useSocket) { Debug._remoteLog("", "clear"); } if (_hasConsole) console.clear(); if (Debug.output) { Debug.output.innerHTML = ""; } } return Debug; }; /** * Generate a stack track in the output * @static * @public * @method trace * @param {*} params Optional parameters to log * @return {Debug} The instance of debug for chaining */ Debug.trace = function(params) { if (Debug.enabled) { if (_useSocket) { Debug._remoteLog(Array.prototype.slice.call(arguments), Levels.GENERAL); } else if (_hasConsole) { if (arguments.length === 1) console.trace(params); else console.trace.apply(console, arguments); } } return Debug; }; /** * Starts a new logging group with an optional title. All console output that * occurs after calling this method and calling `Debug.groupEnd()` appears in * the same visual group. * @static * @public * @method group * @param {*} params Optional parameters to log * @return {Debug} The instance of debug for chaining */ Debug.group = function(params) { if (Debug.enabled) { if (_useSocket) { Debug._remoteLog(Array.prototype.slice.call(arguments), "group"); } else if (_hasConsole && console.group) console.group.apply(console, arguments); } return Debug; }; /** * Creates a new logging group that is initially collapsed instead of open, * as with `Debug.group()`. * @static * @public * @method groupCollapsed * @param {*} params Optional parameters to log * @return {Debug} The instance of debug for chaining */ Debug.groupCollapsed = function(params) { if (Debug.enabled) { if (_useSocket) { Debug._remoteLog(Array.prototype.slice.call(arguments), "groupCollapsed"); } else if (_hasConsole && console.groupCollapsed) console.groupCollapsed.apply(console, arguments); } return Debug; }; /** * Starts a new logging group with an optional title. All console output that * occurs after calling this method and calling console.groupEnd() appears in * the same visual group. * @static * @public * @method groupEnd * @return {Debug} The instance of debug for chaining */ Debug.groupEnd = function() { if (Debug.enabled) { if (_useSocket) { Debug._remoteLog(Array.prototype.slice.call(arguments), "groupEnd"); } else if (_hasConsole && console.groupEnd) console.groupEnd(); } return Debug; }; /** * List of hex colors to create Debug shortcuts for. * Each key will become a function Debug[key]() that outputs * the message in the specified color to the console if * the browsers allows colored logging. * Color Palette pulled from "Better CSS Defaults" * (https://github.com/mrmrs/colors) * * @private * @property {Object} _palette */ var _palette = { /** * Output a general log colored as navy * @method navy * @static * @param {*} message The message to log * @return {Debug} The instance of debug for chaining */ navy: '#001F3F', /** * Output a general log colored as blue * @method blue * @static * @param {*} message The message to log * @return {Debug} The instance of debug for chaining */ blue: '#0074D9', /** * Output a general log colored as aqua * @method aqua * @static * @param {*} message The message to log * @return {Debug} The instance of debug for chaining */ aqua: '#7FDBFF', /** * Output a general log colored as teal * @method teal * @static * @param {*} message The message to log * @return {Debug} The instance of debug for chaining */ teal: '#39CCCC', /** * Output a general log colored as olive * @method olive * @param {*} message The message to log * @return {Debug} The instance of debug for chaining */ olive: '#3D9970', /** * Output a general log colored as green * @method green * @static * @param {*} message The message to log * @return {Debug} The instance of debug for chaining */ green: '#2ECC40', /** * Output a general log colored as lime * @method lime * @static * @param {*} message The message to log * @return {Debug} The instance of debug for chaining */ lime: '#01FF70', /** * Output a general log colored as yellow * @method yellow * @static * @param {*} message The message to log * @return {Debug} The instance of debug for chaining */ yellow: '#FFDC00', /** * Output a general log colored as orange * @method orange * @static * @param {*} message The message to log * @return {Debug} The instance of debug for chaining */ orange: '#FF851B', /** * Output a general log colored as red * @method red * @static * @param {*} message The message to log * @return {Debug} The instance of debug for chaining */ red: '#FF4136', /** * Output a general log colored as pink * @method pink * @static * @param {*} message The message to log * @return {Debug} The instance of debug for chaining */ pink: '#F012BE', /** * Output a general log colored as purple * @method purple * @static * @param {*} message The message to log * @return {Debug} The instance of debug for chaining */ purple: '#B10DC9', /** * Output a general log colored as maroon * @method maroon * @static * @param {*} message The message to log * @return {Debug} The instance of debug for chaining */ maroon: '#85144B', /** * Output a general log colored as silver * @method silver * @static * @param {*} message The message to log * @return {Debug} The instance of debug for chaining */ silver: '#ddd', /** * Output a general log colored as gray * @method gray * @static * @param {*} message The message to log * @return {Debug} The instance of debug for chaining */ gray: '#aaa' }; // Loop through each item in the _palette object and create // a static function in Debug via the key (the color name) that // outputs a message to the console in key's value (a hex color). for (var key in _palette) { if (_consoleSupportsColors) Debug[key] = _colorClosure(_palette[key]); else Debug[key] = Debug.log; } /** * Due to the way closures and variables work, _colorClosure returns * the color logging function needed for the color that you pass it. * * @method _colorClosure * @private * @param {String} hex Hex value to apply to CSS color * @return {Function} */ function _colorClosure(hex) { var colorString = 'color:' + hex; return function(message) { if (arguments.length > 1) { var params = slice.call(arguments); if (typeof params[0] == "object") { params.unshift(colorString); params.unshift('%c%o'); } else { var first = '%c' + params[0]; params[0] = colorString; params.unshift(first); } return Debug.log.apply(Debug, params); } if (typeof arguments[0] == "object") return Debug.log('%c%o', colorString, message); return Debug.log('%c' + message, colorString); }; } //Assign to namespace namespace('springroll').Debug = Debug; }());