From f90869e370c580f1054c77acd6e2c5238fd1203c Mon Sep 17 00:00:00 2001 From: unkelpehr Date: Mon, 22 Sep 2014 01:04:52 +0200 Subject: [PATCH] Base for multi-host support Must do further testing; it's too late for me to continue now. --- public/css/default.css | 30 +- public/index.html | 5 +- public/js/bootstrap.js | 3 + public/js/cryptalk_modules/$.js | 47 ++- public/js/cryptalk_modules/cryptalk.js | 355 +++++++++++++++------ public/js/cryptalk_modules/hosts.js | 15 + public/js/cryptalk_modules/settings.js | 3 - public/js/cryptalk_modules/templates.js | 11 +- public/js/vendor/fandango.v20140921.min.js | 30 +- 9 files changed, 372 insertions(+), 127 deletions(-) create mode 100644 public/js/cryptalk_modules/hosts.js diff --git a/public/css/default.css b/public/css/default.css index cd90c6d..b105b58 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -24,6 +24,12 @@ body, html { color: #00DD00; } +.good { color: #99FF99; } +.bad { color: #ff7777; } +.info { color:#99FFFF; } +.neutral { color: #eeeeee; } + + /*------------------------------------*\ CHAT \*------------------------------------*/ @@ -58,7 +64,7 @@ body, html { #chat i.fatal { color: #ff7777; } /*------------------------------------*\ - INPUT + INPUT & LOADER \*------------------------------------*/ #input_wrapper { right:0; @@ -69,7 +75,8 @@ body, html { height:30px; } -#input { +#input, +#loader { top: 0; bottom: 0; width: 100%; @@ -78,9 +85,28 @@ body, html { border: 0; outline: 0; + + padding: 5px 5px 5px 15px; color: #FFFFFF; background-color:#141414; height:30px; +} + +#input { z-index: 1; } +#loader { z-index: 0; line-height: 20px; font-size: 14px; font-weight: 100; font-family: tahoma;} + +/*------------------------------------*\ + SPINNER +\*------------------------------------*/ +.loading #loader { z-index: 2; } +.loading #loader span { + margin-left:-2px; + -webkit-animation: rotation 1s infinite linear; +} + +@-webkit-keyframes rotation { + from {-webkit-transform: rotate(0deg);} + to {-webkit-transform: rotate(359deg);} } \ No newline at end of file diff --git a/public/index.html b/public/index.html index 79e1424..91b49d9 100644 --- a/public/index.html +++ b/public/index.html @@ -15,10 +15,11 @@ -
+
+
|
- + diff --git a/public/js/bootstrap.js b/public/js/bootstrap.js index d0e23d7..577db78 100644 --- a/public/js/bootstrap.js +++ b/public/js/bootstrap.js @@ -5,6 +5,9 @@ fandango.defaults({ baseUrl: 'js/cryptalk_modules/', paths: { websocket: 'https://cdnjs.cloudflare.com/ajax/libs/socket.io/0.9.16/socket.io.min.js', + // Newer version: + // We'll have to fix the Access Control issue first though (https://github.com/Automattic/socket.io-client/issues/641). + // websocket: 'https://cdn.socket.io/socket.io-1.1.0.js', aes: 'https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/aes.js', domReady: 'https://cdnjs.cloudflare.com/ajax/libs/require-domReady/2.0.1/domReady.min.js' }, diff --git a/public/js/cryptalk_modules/$.js b/public/js/cryptalk_modules/$.js index 5ec45a5..22f071b 100644 --- a/public/js/cryptalk_modules/$.js +++ b/public/js/cryptalk_modules/$.js @@ -9,7 +9,16 @@ define(['fandango', 'websocket', 'aes'], function (fandango, websocket, aes) { utils = exports.utilities, proto = exports.prototype, - each = fandango.each; + each = fandango.each, + + /** + * Regex for matching NaN. + * + * @property reNaN + * @type {Regex} + * @private + */ + reDigits = /^\d+$/; // The DOM selector engine exports.selector = function (selector) { @@ -61,6 +70,20 @@ define(['fandango', 'websocket', 'aes'], function (fandango, websocket, aes) { try { return document.activeElement; } catch (e) {} } + /** + * Removes all characters but 0 - 9 from given string. + * + * @method digits + * @param {String} str The string to sanitize + * @return {String} The sanitized string + * @example + * $.digits('foo8bar'); // `8` + * $.digits('->#5*duckM4N!!!111'); // `54111` + */ + utils.isDigits = function(value) { + return reDigits.test(value); + }; + /** * A very simple templating function. * @param {} str [description] @@ -73,6 +96,28 @@ define(['fandango', 'websocket', 'aes'], function (fandango, websocket, aes) { }); }; + utils.getJSON = function (path, onSuccess, onError) { + var data, request = new XMLHttpRequest(); + request.open('GET', path, true); + + request.onreadystatechange = function() { + if (this.readyState === 4) { + if (this.status >= 200 && this.status < 400) { + try { + onSuccess && onSuccess(JSON.parse(this.responseText)); + } catch (e) { + onError && onError(); + } + } else { + onError && onError(); + } + } + }; + + request.send(); + request = null; + }; + // Part of this is originating from mustasche.js // Code: https://github.com/janl/mustache.js/blob/master/mustache.js#L43 // License: https://github.com/janl/mustache.js/blob/master/LICENSE diff --git a/public/js/cryptalk_modules/cryptalk.js b/public/js/cryptalk_modules/cryptalk.js index d444563..621a012 100644 --- a/public/js/cryptalk_modules/cryptalk.js +++ b/public/js/cryptalk_modules/cryptalk.js @@ -1,7 +1,7 @@ // Main cryptalk module define({ compiles: ['$'], - requires: ['settings', 'templates', 'sound', 'fandango'] + requires: ['hosts', 'templates', 'sound', 'fandango'] }, function ($, requires, data) { var socket, key, @@ -9,8 +9,9 @@ define({ room, hash, nick, + mute, - mute = false, + settings = {}, history = [], history_pos = -1, @@ -20,15 +21,27 @@ define({ // Collection of DOM components components = { chat: $('#chat'), - input: $('#input') + input: $('#input'), + inputWrapper: $('#input_wrapper') }, // Shortcut - settings = requires.settings; + hosts = requires.hosts.hosts; fandango = requires.fandango; templates = requires.templates; sound = requires.sound; + lockInput = function () { + components.input[0].setAttribute('disabled', 'disabled'); + components.inputWrapper[0].className = 'loading'; + }, + + unlockInput = function () { + components.input[0].removeAttribute('disabled'); + components.inputWrapper[0].className = ''; + components.input.focus(); + }, + // Adds a new message to the DOM post = function (type, text, clearChat, clearBuffer, nick) { var tpl = templates.post[type], @@ -53,8 +66,205 @@ define({ // Chat related commands commands = { - help: function () { + help: function (payload, done) { post('motd', templates.help); + done(); + }, + + hosts: function (force, done) { + var i = 0, + left = hosts.length, + host, + strhosts = '\n', + callback = function (host, index, isUp) { + return function (hostSettings) { + host.settings = (isUp ? hostSettings : 0); + + strhosts += $.template(templates.messages[(isUp ? 'host_available' : 'host_unavailable')], { + name: host.name, + path: host.path, + index: index + }); + + if (!--left) { + post('info', strhosts); + done(); + } + } + }; + + // + force = (force && force.toLowerCase() === 'force'); + + // Loop through all the hosts + while (host = hosts[i]) { + if (!force && host.settings !== undefined) { + if (host.settings) { + callback(host, i, 1)(); + } else { + callback(host, i, 0)(); + } + } else { + require([host.path], callback(host, i, 1), callback(host, i, 0)); + } + + i++; + } + }, + + connect: function (toHost, done) { + var request; + + if (host && host.connected) { + done(); + return post('error', $.template(templates.messages.already_connected, { + host: host.name || 'localhost' + })); + } + + if ($.isDigits(toHost)) { + if (host = hosts[+toHost]) { + if (host.settings) { + settings = host.settings; + } else { + request = host.path; + } + } else { + return post('error', 'Undefined host index: ' + toHost); + } + } else if (fandango.is(toHost, 'untyped')) { + settings = toHost.settings; + } else { // Assume string + request = toHost; + } + + if (request) { + return require([request], function (settings) { + host.settings = settings; + commands.connect(toHost, done); + }, function () { + return post('error', 'Could not fetch host settings: ' + request); + }); + } + + // Push 'Connecting...' message + post('info', $.template(templates.messages.connecting, { + host: host.name || 'localhost' + })); + + // The one and only socket + socket = $.Websocket.connect(host.host, { + forceNew: true, + 'force new connection': true + }); + + // Bind socket events + socket + .on('room:generated', function (data) { + var sanitized = $.escapeHtml(data); + post('server', $.template(templates.server.room_generated, { payload: sanitized })); + socket.emit('room:join', sanitized); + }) + + .on('room:joined', function (data) { + room = data; + post('info', $.template(templates.messages.joined_room, { roomName: room })); + + // Automatically count persons on join + socket.emit('room:count'); + }) + + .on('room:left', function () { + post('info', $.template(templates.messages.left_room, { roomName: room })); + + // Clear history on leaving room + clearHistory(); + + room = false; + }) + + .on('message:send', function (data) { + var decrypted = $.AES.decrypt(data.msg, room + key), + sanitized = $.escapeHtml(decrypted), + nick = (data.nick == undefined || !data.nick ) ? templates.default_nick : $.escapeHtml($.AES.decrypt(data.nick, room + key)); + + if (!decrypted) { + post('error', templates.messages.unable_to_decrypt); + } else { + post('message', sanitized, false, false, nick); + if( !mute ) sound.playTones(sound.messages.message); + } + }) + + .on('message:server', function (data) { + if( data.msg ) { + var sanitized = $.escapeHtml(data.msg); + if( templates.server[sanitized] ) { + if( data.payload !== undefined ) { + var sanitized_payload = $.escapeHtml(data.payload); + post('server', $.template(templates.server[sanitized], { payload: sanitized_payload })); + } else { + post('server', templates.server[sanitized]); + } + + // Play sound + if (sound.messages[sanitized] !== undefined && !mute ) sound.playTones(sound.messages[sanitized]); + + } else { + post('error', templates.server.bogus); + } + } else { + post('error', templates.server.bogus); + } + }) + + .on('connect', function () { + // Tell the user that the chat is ready to interact with + post('info', $.template(templates.messages.connected, { + host: host.name || 'localhost' + })); + + host.connected = 1; + + done(); + }) + + .on('disconnect', function () { + room = 0; + key = 0; + host.connected = 0; + + // Tell the user that the chat is ready to interact with + post('info', $.template(templates.messages.disconnected, { + host: host.name || 'localhost' + })); + }) + + .on('error', function () { + room = 0; + key = 0; + host.connected = 0; + post('error', templates.messages.socket_error); + done(); + }); + }, + + reconnect: function (foo, done) { + if (host) { + if (host.connected) { + commands.disconnect() + commands.connect(host, done); + } else { + commands.connect(host, done); + } + } else { + done(); + return post('error', templates.messages.reconnect_no_host); + } + }, + + disconnect: function () { + socket.disconnect(); }, clear: function () { @@ -81,6 +291,10 @@ define({ }, key: function (payload) { + if (!host) { + return post('error', templates.messages.key_no_host); + } + // Make sure the key meets the length requirements if (payload.length > settings.key_maxLen) { return post('error', templates.messages.key_to_long); @@ -119,6 +333,10 @@ define({ }, join: function (payload) { + if (!host) { + return post('error', templates.messages.join_no_host); + } + return ( room ? post('error', templates.messages.already_in_room) @@ -169,7 +387,7 @@ define({ // The Document object is bound to this element. // If the active element is not the input, focus on it and exit the function. // Ignore this when ctrl and/or alt is pressed! - if (components.input[0] !== $.activeElement() && !e.ctrlKey && !e.altKey) { + if (!e.ctrlKey && !e.altKey && components.input[0] !== $.activeElement()) { return components.input.focus(); } @@ -178,7 +396,7 @@ define({ history_timer = setTimeout(clearHistory, 60000); // Check for escape key, this does nothing but clear the input buffer and reset history position - if ( e.keyCode == 27 ) { + if (e.keyCode == 27) { history_pos = -1; clearInput(); @@ -186,7 +404,7 @@ define({ } // Check for up or down-keys, they handle the history position - if( e.keyCode == 38 || e.keyCode == 40) { + if (e.keyCode == 38 || e.keyCode == 40) { if (e.keyCode == 38 ) { history_pos = (history_pos > history.length - 2) ? -1 : history_pos = history_pos + 1; } else { history_pos = (history_pos <= 0) ? -1 : history_pos = history_pos - 1; } @@ -220,8 +438,18 @@ define({ return post('error', $.template(templates.messages.unrecognized_command, { commandName: command })); } - // Execute command handler - commands[command](payload); + // Some commands are asynchrounous; + // If the command expects more than one argument, the second argument is a callback that is called when the command is done. + if (commands[command].length > 1) { + // Lock the input from further interaction + lockInput(); + + // Execute command handler with callback function. + commands[command](payload, unlockInput); + } else { + // Execute normally. + commands[command](payload); + } // Clear input field clearInput(); @@ -257,103 +485,24 @@ define({ } }; - host = settings.host; + // Bind the necessary DOM events + $(document).on('keydown', onKeyDown); + + // Put focus on the message input + components.input.focus(); // Post the help/welcome message post('motd', templates.motd, true); - // Push 'Connecting...' message - post('info', $.template(templates.messages.connecting, { - host: host || 'localhost' - })); + unlockInput(); - // The one and only socket - socket = $.Websocket.connect(host); + // It's possible to provide room and key using the hashtag. + // The room and key is then seperated by semicolon (room:key). + // If there is no semicolon present, the complete hash will be treated as the room name and the key has to be set manually. + if (host && (hash = window.location.hash)) { + parts = hash.slice(1).split(':'); - // Bind socket events - socket - .on('room:generated', function (data) { - var sanitized = $.escapeHtml(data); - post('server', $.template(templates.server.room_generated, { payload: sanitized })); - socket.emit('room:join', sanitized); - }) - - .on('room:joined', function (data) { - room = data; - post('info', $.template(templates.messages.joined_room, { roomName: room })); - - // Automatically count persons on join - socket.emit('room:count'); - }) - - .on('room:left', function () { - post('info', $.template(templates.messages.left_room, { roomName: room })); - - // Clear history on leaving room - clearHistory(); - - room = false; - }) - - .on('message:send', function (data) { - var decrypted = $.AES.decrypt(data.msg, room + key), - sanitized = $.escapeHtml(decrypted), - nick = (data.nick == undefined || !data.nick ) ? templates.default_nick : $.escapeHtml($.AES.decrypt(data.nick, room + key)); - - if (!decrypted) { - post('error', templates.messages.unable_to_decrypt); - } else { - post('message', sanitized, false, false, nick); - if( !mute ) sound.playTones(sound.messages.message); - } - }) - - .on('message:server', function (data) { - if( data.msg ) { - var sanitized = $.escapeHtml(data.msg); - if( templates.server[sanitized] ) { - if( data.payload !== undefined ) { - var sanitized_payload = $.escapeHtml(data.payload); - post('server', $.template(templates.server[sanitized], { payload: sanitized_payload })); - } else { - post('server', templates.server[sanitized]); - } - - // Play sound - if (sound.messages[sanitized] !== undefined && !mute ) sound.playTones(sound.messages[sanitized]); - - } else { - post('error', templates.server.bogus); - } - } else { - post('error', templates.server.bogus); - } - }) - - .on('connect', function () { - // Bind the necessary DOM events - $(document).on('keydown', onKeyDown); - - // Put focus on the message input - components.input.focus(); - - // Tell the user that the chat is ready to interact with - post('info', $.template(templates.messages.connected, { - host: host || 'localhost' - })); - - // It's possible to provide room and key using the hashtag. - // The room and key is then seperated by semicolon (room:key). - // If there is no semicolon present, the complete hash will be treated as the room name and the key has to be set manually. - if (hash = window.location.hash) { - parts = hash.slice(1).split(':'); - - parts[0] && commands.join(parts[0]); - parts[1] && commands.key(parts[1]); - } - }) - - .on('error', function () { - post('error', templates.messages.socket_error); - }); + parts[0] && commands.join(parts[0]); + parts[1] && commands.key(parts[1]); + } }); \ No newline at end of file diff --git a/public/js/cryptalk_modules/hosts.js b/public/js/cryptalk_modules/hosts.js new file mode 100644 index 0000000..ea61b1a --- /dev/null +++ b/public/js/cryptalk_modules/hosts.js @@ -0,0 +1,15 @@ +define({ + // Used to autoconnect to specific host. + // Points to a specific index in the 'hosts' array. + // Use -1 to not autoconnect. + autoconnect: 0, + + // A collection of hosts to choose from + hosts: [ + { + name: 'localhost', + host: 'http://localhost:8080', + path: 'http://localhost:8080/js/cryptalk_modules/settings.js' + } + ] +}); \ No newline at end of file diff --git a/public/js/cryptalk_modules/settings.js b/public/js/cryptalk_modules/settings.js index 7510604..a54b96d 100644 --- a/public/js/cryptalk_modules/settings.js +++ b/public/js/cryptalk_modules/settings.js @@ -1,7 +1,4 @@ define({ - // If no host is given it will default to localhost. - host: '', - nick_maxLen: 20, nick_minLen: 3, diff --git a/public/js/cryptalk_modules/templates.js b/public/js/cryptalk_modules/templates.js index df50fe5..7e61386 100644 --- a/public/js/cryptalk_modules/templates.js +++ b/public/js/cryptalk_modules/templates.js @@ -65,6 +65,9 @@ define({ key_to_long: 'Man that\'s a long key. Make it a tad short, \'kay?', key_ok_ready: 'Key set, you can now start communicating.', key_ok_but_no_room: 'Key set, you can now join a room and start communicating.', + key_no_host: 'You have to connect to a host before setting the key.', + + join_no_host: 'You have to connect to a host before joining a room.', nick_to_short: 'Nickname is too short, it has to be at least {nick_minLen} characters long. Try again.', nick_to_long: 'Nickname is too long, it can be at most {nick_maxLen} characters long. Try again.', @@ -90,7 +93,13 @@ define({ socket_error: 'A network error has occurred. A restart may be required to bring back full functionality.
Examine the logs for more details.', connecting: 'Connecting to host {host}...', - connected: 'A connection to the server has been established. Happy chatting!' + connected: 'A connection to the server has been established. Happy chatting!', + disconnected: 'Disconnected from host {host}.', + already_connected: 'You have to disconnect from {host} before joining another.', + reconnect_no_host: 'There is no host to reconnect with.', + + host_available: '{index} [AVAILABLE] {name}\n', + host_unavailable: '{index} [UNAVAILABLE] {name}\n' }, server: { diff --git a/public/js/vendor/fandango.v20140921.min.js b/public/js/vendor/fandango.v20140921.min.js index d284fa5..50e73c8 100644 --- a/public/js/vendor/fandango.v20140921.min.js +++ b/public/js/vendor/fandango.v20140921.min.js @@ -1,20 +1,20 @@ -(function(r){function A(f){this.name="TimeoutError";this.message=f||""}function B(f){this.name="RejectedError";this.message=f||""}function F(f,c,e,g){var d,a,b=[],m,p,k,h=function(a){f.length===b.push(l[a].exports)&&(g&&clearTimeout(m),c(b))};g=g||v.timeout;for(k=0;p=f[k++];)if((d=l[p])&&1!==d.state)if(2===d.state)if(a=new B('Could resolve all dependencies; dependency "'+p+'" has been rejected.'),e)e(a);else throw a;else 3===d.state&&h(p);else(u[p]=u[p]||[]).push(h);g&&(m=setTimeout(function(){for(var c, -d,m=0,p=0;c=f[m++];)for(p=0;d=b[p++];)c===d&&(b.splice(--p,1),f.splice(--m,1));a=new A("Load timeout of "+g+'ms exceeded for module(s) "'+f.join('", "')+'".');if(e)e(a);else throw a;},g))}function y(){var f=!1,c,e,g,d,a,b,m;if(arguments[0]!==C){for(c=0;(e=arguments[c++])&&(_type=(typeof e)[0]);)if("s"===_type?d?g=1:d=e:"f"===_type?b?m?g=1:m=e:b=e:"o"===_type&&(h.is(e,"array")?(a={requires:e},f=!0):a?b=e:a=e),g)throw new TypeError("define called with unrecognized signature; `"+Array.prototype.join.call(arguments, -", ")+"`.");a=a||{};d=d||a.UID;b=b||a.factory;m=m||a.onRejected}else if(c=D.pop())d=arguments[1],a=c[0],b=c[1],m=c[2],f=c[3];else throw Error("Inconsistent naming queue");if(!b)if(a)b=a,a={};else throw Error('Missing factory for module "'+d+'"');if(l[d])throw Error('Duplicate entry for module "'+d+'"');d?(c=l[d]=a,c.UID=d,c.amdStyle=f,c.factory=b,c.state=1,c.requires=h.is(c.requires,"array")&&c.requires,c.inherits=h.is(c.inherits,"array")&&c.inherits,c.compiles=h.is(c.compiles,"array")&&c.compiles, -c.instances||(c.instances={instance1:{}})):D.unshift([a,b,m,f])}var l={},v={deepCopy:!0,baseUrl:"",namespace:"default",timeout:1E3,paths:{},shim:{}},h={},C={a:1},u={},D=[],z=Array.prototype.push;A.prototype=Error.prototype;B.prototype=Error.prototype;h.is=function(){var f=Object.prototype.hasOwnProperty,c=Object.prototype.toString,e={array:Array.isArray||function(a){return"[object Array]"==c.call(a)},arraylike:function(a){if(!a||!a.length&&0!==a.length||e.window(a))return!1;var b=a.length;return 1=== +(function(s){function A(f){this.name="TimeoutError";this.message=f||""}function B(f){this.name="RejectedError";this.message=f||""}function F(f,c,e,g){var d,a,b=[],n,m,k,h=function(a){f.length===b.push(l[a].exports)&&(g&&clearTimeout(n),c(b))};g=g||v.timeout;for(k=0;m=f[k++];)if((d=l[m])&&1!==d.state)if(2===d.state)if(a=new B('Could resolve all dependencies; dependency "'+m+'" has been rejected.'),e)e(a);else throw a;else 3===d.state&&h(m);else(u[m]=u[m]||[]).push(h);g&&(n=setTimeout(function(){for(var d, +c,n=0,m=0;d=f[n++];)for(m=0;c=b[m++];)d===c&&(b.splice(--m,1),f.splice(--n,1));a=new A("Load timeout of "+g+'ms exceeded for module(s) "'+f.join('", "')+'".');if(e)e(a);else throw a;},g))}function y(){var f=!1,c,e,g,d,a,b,n;if(arguments[0]!==C){for(c=0;(e=arguments[c++])&&(_type=(typeof e)[0]);)if("s"===_type?d?g=1:d=e:"f"===_type?b?n?g=1:n=e:b=e:"o"===_type&&(h.is(e,"array")?(a={requires:e},f=!0):a?b=e:a=e),g)throw new TypeError("define called with unrecognized signature; `"+Array.prototype.join.call(arguments, +", ")+"`.");a=a||{};d=d||a.UID;b=b||a.factory;n=n||a.onRejected}else if(c=D.pop())d=arguments[1],a=c[0],b=c[1],n=c[2],f=c[3];else throw Error("Inconsistent naming queue");if(!b)if(a)b=a,a={};else throw Error('Missing factory for module "'+d+'"');if(l[d])throw Error('Duplicate entry for module "'+d+'"');d?(c=l[d]=a,c.UID=d,c.amdStyle=f,c.factory=b,c.state=1,c.requires=h.is(c.requires,"array")&&c.requires,c.inherits=h.is(c.inherits,"array")&&c.inherits,c.compiles=h.is(c.compiles,"array")&&c.compiles, +c.instances||(c.instances={instance1:{}})):D.unshift([a,b,n,f])}var l={},v={deepCopy:!0,baseUrl:"",namespace:"default",timeout:1E3,paths:{},shim:{}},h={},C={a:1},u={},D=[],z=Array.prototype.push;A.prototype=Error.prototype;B.prototype=Error.prototype;h.is=function(){var f=Object.prototype.hasOwnProperty,c=Object.prototype.toString,e={array:Array.isArray||function(a){return"[object Array]"==c.call(a)},arraylike:function(a){if(!a||!a.length&&0!==a.length||e.window(a))return!1;var b=a.length;return 1=== a.nodeType||e.array(a)||!e["function"](a)&&(0===b||"number"===typeof b&&0a&&Math.floor(a)===a},iterable:function(a){try{1 in obj}catch(b){return!1}return!0},nan:function(a){return e.number(a)&& a!=+a},number:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},object:function(a){return a===Object(a)},primitive:function(a){return!0===a||!1===a||null==a||!!{string:1,number:1}[typeof a]},string:function(a){return"string"==typeof a||a instanceof String},undefined:function(a){return void 0===a},untyped:function(a){if(!a||a.nodeType||"[object Object]"!==c.call(a)||e.window(a))return!1;try{if(a.constructor&&!f.call(a,"constructor")&&!f.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(b){return!1}for(var d in a); return void 0===d||f.call(a,d)},window:function(a){return null!=a&&a==a.window},empty:function(a){if(a){if(h.is(a,"array"))return 0===a.length;for(var b in a)if(f.call(a,b))return!1}return!0}},g=0,d=["Arguments","Date","Function","RegExp"];for(;4>g;g++)e[d[g].toLowerCase()]=function(a){a="[object "+a+"]";return function(b){return c.call(b)==a}}(d[g]);e.args=e.arguments;e.bool=e["boolean"];e.plain=e.untyped;return function(a,b){if(e["function"](b))return a===b(a);if(!e.string(b))return a===b;if((b= -b.toLowerCase())&&e[b])return e[b](a);throw'Unknown type "'+b+'"';}}();h.each=function(){var f=Array.prototype.some;return function(c,e,g){var d,a;if(void 0===c)return obj;g=g||h;if(f&&c.some===f)return c.some(e,g),c;if(h.is(c,"array")||h.is(c,"arraylike")){d=0;for(a=c.length;dt.length&&t.push(d.data?h.merge(d.deepCopy,{},d.data,b.data):b.data),p>t.length&&t.push(h.merge(d.deepCopy,{},d,b))),d.exports=m?b.factory.apply(d.context,t)||{}:b.factory,void 0===b.exports&&(b.exports=g?d.exports?h.merge(!0,{},g,d.exports):g:d.exports);b.state=3;if(u[c])for(;u[c].length;)u[c].pop()(c)}return function e(g, -d){var a,b=l[g],m=h.is(b.factory,"function")?b.factory.length:0,p=b.amdStyle,k=[],t,s,r;a=[];var q,n;if(!d&&(b.requires||b.compiles||b.inherits)){b.requires&&z.apply(a,b.requires);b.inherits&&z.apply(a,b.inherits);b.compiles&&z.apply(a,b.compiles);for(q=0;n=a[q++];)if(l[n])if(3===l[n].state)a.splice(--q,1);else if(2===l[n].state)throw Error('Could not instantiate "'+UID+'"; dependency "'+dependency+'" has been rejected.');if(0t.length&&t.push(d.data?h.merge(d.deepCopy,{},d.data,b.data):b.data),m>t.length&&t.push(h.merge(d.deepCopy,{},d,b))),d.exports=n?b.factory.apply(d.context,t)||{}:b.factory,void 0===b.exports&&(b.exports=g?d.exports?h.merge(!0,{},g,d.exports):g:d.exports);b.state=3;if(u[c])for(;u[c].length;)u[c].pop()(c)}return function e(g, +d){var a,b=l[g],n=h.is(b.factory,"function")?b.factory.length:0,m=b.amdStyle,k=[],t,q,s;a=[];var r,p;if(!d&&(b.requires||b.compiles||b.inherits)){b.requires&&z.apply(a,b.requires);b.inherits&&z.apply(a,b.inherits);b.compiles&&z.apply(a,b.compiles);for(r=0;p=a[r++];)if(l[p])if(3===l[p].state)a.splice(--r,1);else if(2===l[p].state)throw Error('Could not instantiate "'+UID+'"; dependency "'+dependency+'" has been rejected.');if(0