Source: js/library.functions.js

/** @function autoExpand
    @desc     Used as a callback on keydown/press in textareas,
              resizes them to accommodate full height of text while typing.
    
    @arg      {Element|jQuery} $element - Textarea to resize.
    @arg      {Function}       callback - Function to call with new height after resizing.
*/
function autoExpand($element, callback)
{
    element  = jUnwrap($element);
    callback = callback || function () {};
    
    //element.style.height = (element.scrollHeight > element.clientHeight) ? (element.scrollHeight) + 'px' : '1.5rem';
    
    element.style.height = '0px';
    
    setTimeout(function ()
    {
        var height = element.style.height = (element.scrollHeight) + 'px';
        callback.apply(element, [height]);
    },
    1);
}



/** @function convertToLocalTime
    @desc     Adds local timezone offset to a numeric UTC timestamp.
    
    @arg      {Number} timeUTC - UTC timestamp.
    @returns  {Object}           New Date instance.
*/
function convertToLocalTime(timeUTC)
{
    var offset    = parseInt(new Date().getTimezoneOffset()),
        timeLocal = new Date(timeUTC.getTime() - (offset * T_MINUTE));
    
    return timeLocal;
}



/** @function dataArrayToObject
    @desc     Transforms a list of data objects into an object of data objects. Each key in the containing object
              corresponds to a specific property value of each data object. Each data object must contain the property
              used for indexing. Useful for fast lookups without looping.
    
    @arg      {Array}  data    - List of data to transform.
    @returns  {String} indexed - Property within each data object whose value to use as key in indexed object.
    @returns  {Object}           Indexed data object.
    
    @see      {@link dataObjectToArray}
*/
function dataArrayToObject(data, indexed)
{
    var funcName = 'window.dataArrayToObject',
        type     = dltypeof(data),
        indexed  = dltypeof(indexed) === 'string' ? indexed : false,
        dataNew  = {};
    
    // validate
    
    if (!indexed)         { log(L_ERROR, funcName, 'invalid indexing key'); return false;   }
    if (type !== 'array') { log(L_ERROR, funcName, 'data is not an array'); return false;   }
    if (!data.length)     { log(L_INFO , funcName, 'data is empty');        return dataNew; }
    
    // convert
    
    for (var i = 0; i < data.length; i++)
    {
        var item = data[i],
            key  = item[indexed];
        
        if (typeof key !== 'undefined') { dataNew[key] = item; } else
        {
            log(L_ERROR, funcName, 'data index {0} lacks key "{1}"'.format(i, indexed));
            return false;
        }
    }
    
    return dataNew;
}



/** @function dataObjectToArray
    @desc     Transform indexed data object into plain list of data objects, optionally populating each with value
              formerly used as keys.
    
    @arg      {Object} data      - Indexed data object.
    @arg      {String} [indexed] - Property within each data object to restore using containing object's keys as values.
    @returns  {Array}              List of data objects. 
    
    @see      {@link dataArrayToObject}
    @see      {@link Object#toArray}
*/
function dataObjectToArray(data, indexed)
{
    var funcName = 'window.dataObjectToArray',
        type     = dltypeof(data),
        indexed  = dltypeof(indexed) === 'string' ? indexed : false,
        dataNew  = [];
    
    // validate
    
    if (!~$.inArray(type, ['object', 'jsobject'])) { log(L_ERROR, funcName, 'data is not an object'); return false; }
    
    // convert
    
    for (var key in data) { if (data.hasOwnProperty(key))
    {
        var item = data[key];
        
        if (!~$.inArray(dltypeof(item), ['object', 'jsobject']))
        {
            log(L_ERROR, funcName, 'data key "{0}" is not an object'.format(key));
            return false;
        }
        
        // add index value to item if asked
        
        indexed
            && typeof item[indexed] === 'undefined'
            && (item[indexed] = key);
        
        dataNew.push(item);
    }}
    
    if (!dataNew.length) { log(L_INFO , funcName, 'data is empty'); }
    
    return dataNew;
}



/** @function decodeURIComponentSafe
    @desc     A version of decodeURIComponent that catches exceptions, allowing code to continue execution.
    
    @arg      {String} encoded - String to decode safely.
    @returns  {String}           Decoded string.
*/
function decodeURIComponentSafe(encoded)
{
    var decoded;
    
    try { decoded = decodeURIComponent(encoded); } catch (e)
    {
        var caller = arguments.callee.caller.toString() // the responsible function body
                         .replace(/[\r\n]/i, '')        // strip line breaks
                         .substr(0, 100);               // truncate
        
        decoded = encoded;
        
        log
        (
            L_ERROR,
            'window.decodeURIComponentSafe',
            'found an unsafe string: "{0}"'.format(encoded),
            'called by: "{0}"'.format(caller)
        );
    }
    
    return decoded;
}



/** @function detectStyles
    @desc     Searches DOM for unprocessed less styleheets, and processes them.
    @returns  {Number} Count of stylesheets detected and processed.
*/
function detectStyles()
{
    var timer       = new TimerClass(),
        found       = 0,
        $lessStyles = $('link[type="text/less"]');
    
    if ($lessStyles.length)
    {
        $lessStyles.each(function ()
        {
            var $link = $(this);
            
            if (!$link.hasClass('processed'))
            {
                $link.addClass('processed');
                less.sheets.push(this);
                found++;
            }
            
            $link.hasClass('processed') && $link.remove();
        });
        
        less.refresh();
    }
    
    log(L_INFO, 'window.detectStyles', '{0} in {1}'.format(found, timer.elapsed(1)));
    
    return found;
}



/** @function detectTemplates
    @desc     Searches document for unprocessed templates, compiles them, and saves generator functions as both
              properties in the globally accessible {@link templates} object,
              and partials usable by id within other templates.
    
    @arg      {String}  prefix        - Used to filter what template ids are to be processed.
    @arg      {Boolean} [force=false] - Whether to replace saved generators if a duplicate is found.
    @returns  {Number}                  Count of templates detected and processed.
*/
function detectTemplates(prefix, force)
{
    var timer     = new TimerClass(),
        prefix    = prefix || '',
        force     = force  || false,
        pattern   = new RegExp('^' + prefix + '(.+)$', 'i'),
        matches   ,
        id        ,
        source    ,
        generator ,
        found     = 0;
    
    $('script[type*="handlebars"]').each(function ()
    {
        if ((matches = this.id.match(pattern)) !== null)
        {
            id = matches[1];
            
            if (!force && (id in window.templates)) { return true; } // continue
            
            var $element = $(this);
            
            log(L_INFO, 'window.detectTemplates', '#' + $element.attr('id'));
            
            source               = $.trim($element.html());
            generator            = Handlebars.compile(source);
            window.templates[id] = generator;       // template
            Handlebars.registerPartial(id, source); // partial
            $element.remove();
            found++;
        }
    });
    
    log(L_INFO, 'window.detectTemplates', found + ' in ' + timer.elapsed(1));
    
    return found;
};



/** @function deval
    @desc     As close as possible to the opposite of eval. Like JSON.stringify without type restrictions.
    
    @arg      {Mixed}  thing - Any value.
    @returns  {String}         Representation of value suitable for direct eval.
*/
function deval(thing)
{
    var self = arguments.callee;
    
    // static
    
    typeof self.whitespace === 'undefined' && (self.whitespace = true);
    typeof self.comments   === 'undefined' && (self.comments   = true);
    
    // private
    
    var s         = self.whitespace ? ' ' : '', // space?
        separator = ',' + s,
        type      = dltypeof(thing) || 'jsobject'; // if dltypeof says undefined, it's probably an object
    
    switch (type) // what is this thing?
    {
        case 'array'             :
            
            var values      = [],
                thingLength = thing.length;
            
            for (var i = 0; i < thingLength; i++) { values.push(self(thing[i])); }
            return '[' + values.join(self.separator) + ']';
            
        case 'object'            :
        case 'jsobject'          :
        case 'arguments'         :
            
            var values = [];
            
            for (key in thing)
            {
                thing.hasOwnProperty(key)
                    && values.push('"' + key + '":' + s + self(thing[key]));
            }
            return '{' + values.join(self.separator) + '}';
            
        case 'boolean'           :
        case 'number'            :
        case 'funciton'          : return thing.toString();
        case 'string'            : return '"' + thing.replace(/\"/g, '\\"') + '"';
        case 'domelement'        : return (s && '/* element */ ') + '$("' + html(thing).replace(/\"/g, '\\"') + '")[0]';
        case 'date'              : return 'new Date(' + thing.getTimestamp() + ')';
        case 'math'              : return 'Math';
        case 'window'            : return 'window';
        case 'undefined'         : return 'undefined';
        case 'null'              : return 'null';
        case 'error'             : return '"[Error]"';
        case 'domcollection'     : return '"[DOM Collection]"';
        case 'mimetypecollection': return '"[Mime Type Collection]"';
        case 'plugincollection'  : return '"[Plugin Collection]"';
        case 'textnode'          : return '"[Text Node]"';
        case 'textrange'         : return '"[Text Range]"';
        case 'regexp'            : return (self.comments ? '/*'+s+'regexp'+s+'*/'+s : '')
                                       + '(function'+s+'(i)'+s+'{var x'+s+'='+s+'/'
                                       +  thing.source + '/'
                                       + (thing.global ? 'g' : '')
                                       + (thing.ignoreCase ? 'i' : '')
                                       + (thing.multiline ? 'm' : '')
                                       + (thing.sticky ? 'y' : '')
                                       + ';'+s+'x.lastIndex'+s+'='+s+'i;'+s+'return x;'+s+'})('
                                       +  thing.lastIndex + ')';
        default                  : return (/^(html|element)/i.test(type)) // html-ifiable?
                                       ? (self.comments ? '/*'+s+'element'+s+'*/'+s : '') + '$("'
                                             + html(thing).replace(/\"/g, '\\"') + '")[0]'
                                       : (self.comments ? '/*'+s+ type +s+'*/' : '') + 'null';
    }
}



// in development
function diff(before, after)
{
    var beforeType = dltypeof(before),
        afterType  = dltypeof(after);
    
    if (beforeType !== afterType)
    {
        log(L_ERROR, 'window.diff', 'types do not match');
        return false;
    }
    
    switch (beforeType)
    {
        case 'object'  :
        case 'jsobject':
            
            var output =
                {
                    'added'   : {},
                    'modified': {},
                    'removed' : {}
                };
            
            for (var prop in before) { if (before.hasOwnProperty(prop))
            {
                
            }}
            
            return;
            
        case 'array':
            
            var output =
            {
                'added'  : after .filter(function (item, index) { return before.indexOf(item) < 0; }),
                'removed': before.filter(function (item, index) { return after .indexOf(item) < 0; })
            };
            
            return output;
        
        default:
            
            log(L_ERROR, 'window.diff', 'unsupported type "{0}"'.format(typeA));
            return false;
    }
}



// in development
function diffDataArray(a, b)
{
    var aType = dltypeof(a),
        bType = dltypeof(b),
        output = {'added': [], 'modified': [], 'deleted': []};
    
    if (aType !== bType)
    {
        log(L_ERROR, 'window.diff', 'types do not match');
        return output;
    }
    
    if (aType !== 'array')
    {
        log(L_ERROR, 'window.diff', 'types are not arrays');
        return output;
    }
    
    var idsGet = function (item, index) { return item.id; },
        ids    = [],
        idsA   = a.map(idsGet),
        idsB   = a.map(idsGet);
    
    $.extend(ids, idsA, idsB);
    
    a = dataArrayToObject(a, 'id');
    b = dataArrayToObject(b, 'id');
    
    for (var id in ids)
    {
             if (typeof a[id] === 'undefined') { output.added.push(b[id]); }
        else if (typeof b[id] === 'undefined') { output.deleted.push(a[id]); }
        else if (diffDataObject(a[id], b[id])) { output.modified.push(b[id]); }
    }
    
    return output;
}



// in development
function diffDataObject(a, b)
{
    var keys     = [],
        keysA    = [],
        keysB    = [],
        modified = false;
    
    for (var key in a) { a.hasOwnProperty(key) && keysA.push(key); }
    for (var key in b) { b.hasOwnProperty(key) && keysB.push(key); }
    
    $.extend(keys, keysA, keysB);
    
    $.each(keys, function (index, value)
    {
        if (a[key] !== b[key]) { modified = true; return false; }
    });
    
    return modified;
}



/** @function exists
    @desc     Checks if a value isn't undefined. Detects false, null, and NaN.
    
    @arg      {Mixed}   thing - Any value.
    @returns  {Boolean}         Whether thing exists.
*/
function exists(thing) { return typeof thing !== 'undefined'; }



/** @function fork
    @desc     Faux-forks a function call via setTimeout.
              This allows procedural execution to continue while the function executes in semi-parallel (round-robin).
              Useful for allowing layout/reflow/repaint so up to date values can be queried from the DOM.
    
    @arg      {Function} func      - Function to call, less-than-procedurally.
    @arg      {Number}   [delay=0] - 
    @returns  {Number}               Positive if f was actually a function and was forked.
*/
function fork(func, delay) { return typeof func === 'function' ? setTimeout(func, Number(delay) || 0) : 0; }



/** @function   format_mysql_date
    @desc       Transforms a timestamp into a mysql-style formatted date string.
    @deprecated since version 1.9
    
    @arg        {Number} date             - Integer UTC timestamp.
    @arg        {String} [day_offset='0'] - Number of days to offset. Actually only responds to '0' vs any other value.
    @returns    {String}                    Formatted date.
*/
function format_mysql_date(date, day_offset)
{
    if (date === '0') { return date; }
    
    var offset         = parseInt(new Date().getTimezoneOffset())
        formatted_date = new Date(date)
        local_time     = new Date
                       (
                           formatted_date.getTime()
                               + offset * T_MINUTE
                               + (day_offset === '0' ? 0 : T_DAY)
                       ),
        month          = parseInt(local_time.getMonth()) + 1,
        mysql_date     = local_time.getFullYear()
                           + '-' + month
                           + '-' + local_time.getDate()
                           + ' ' + local_time.getHours()
                           + ':' + local_time.getMinutes()
                           +' :' + local_time.getSeconds();
    
    return mysql_date;
}



/** @function getAttributes
    @desc     Reads element attributes into an object.
    
    @arg      {jQuery|Element} $element - Source of attributes.
    @returns  {Object}                    Populated object.
*/
function getAttributes($element)
{
    var keys       = jWrap($element)[0].attributes,
        keysLength = keys.length;
        attributes = {};
    
    for (var i = 0; i < keysLength; i++)
    {
        var key = keys[i];
        attributes[key.name] = key.value;
    }
    
    return attributes;
}



/** @function html
    @desc     Transforms element into representative markup.
    
    @arg      {jQuery|Element} element - Element to transform.
    @returns  {String}                   Representative HTML.
*/
function html(element) { return $('<div></div>').append(jWrap(element).clone()).html().replace(/\s{2,}/g, ' '); }



/** @function htmlDecode
    @desc     Unescapes HTML entities within a string.
    
    @arg      {String} html - String to decode.
    @returns  {String}        Decoded string.
*/
function htmlDecode(html) { return $('<div></div>').html(html).text(); }



/** @function htmlEncode
    @desc     Escapes HTML-unsafe characters within a string.
    
    @arg      {String} text - String to encode.
    @returns  {String}        Encoded string.
*/
function htmlEncode(text) { return $('<div></div>').text(text).html(); }



/** @function isExternal
    @desc     Tries to determine if a given URL points within the scope of the current page.
    
    @arg      {String}  url - Location to analyze.
    @returns  {Boolean}       Whether url is external.
*/
function isExternal(url)
{
    var match = url.match(/^([^:\/?#]+:)?(?:\/\/([^\/?#]*))?([^?#]+)?(\?[^#]*)?(#.*)?/);
    
    if
    (
        typeof match[1] === "string"
            && match[1].length > 0
            && match[1].toLowerCase() !== location.protocol
    )
    { return true; }
    
    if
    (
        typeof match[2] === "string"
            && match[2].length > 0
            && match[2].replace(new RegExp(":("+{"http:":80,"https:":443}[location.protocol]+")?$"), "") !== location.host
    )
    { return true; }
    
    return false;
}



/** @function jUnwrap
    @desc     Removes jQuery wrapper from element, if any.
    
    @arg      {jQuery|Element} $element - Reference to strip of jQuery wrapper.
    @returns  {Element}                   Guaranteed bare element.
*/
function jUnwrap($element) { return $element instanceof $ ? $element[0] : $element; }



/** @function jWrap
    @desc     Adds jQuery wrapper to element if not already wrapped.
    
    @arg      {jQuery|Element} $element - Reference to wrap with jQuery.
    @returns  {Element}                   Guaranteed wrapped element.
*/
function jWrap(element) { return element instanceof $ ? element : $(element); }



/** @function killEvent
    @desc     Shorthand way of stopping propagation and preventing default action for a given event.
    
    @arg      {Object} event - Event to neutralize.
*/
function killEvent(event) { event.preventDefault(); event.stopPropagation(); }



/** @function linkCallback
    @desc     Wraps a function reference in a global uniquely named function, and returns that name.
              Useful for passing arbitrary or anonymous functions into Framework methods, which only accept strings.
    
    @arg      {Function} callback         - Function to represent via string.
    @arg      {Object}   [context=window] - <this> value when calling wrapped function.
    @arg      {Boolean}  [persist=false]  - Whether the wrapper should persist after one call, or destruct itself.
    @arg      {Object}   [deferred]       - Deferred object to resolve in addition to calling wrapped function.
    @returns  {String}                      Global callback wrapper function name.
    
    @property {Number}   counter          - How many wrapper functions have been generated.
    @property {Number}   expire           - How long before non-persisting functions automatically destruct themselves.
    
    @see      {@link linkCallback(2)}
    @see      {@link unlinkCallback}
*/
/** @function linkCallback(2)
    @desc     Logs current settings (static properties).
    @returns  {Boolean} false
    
    @property {Number} counter - How many wrapper functions have been generated.
    @property {Number} expire  - How long before non-persisting functions automatically destruct themselves.
    
    @see      {@link linkCallback}
    @see      {@link unlinkCallback}
*/
function linkCallback(callback, context, persist, deferred)
{
    var self           = arguments.callee,
        argumentsLenth = arguments.length;
    
    // static
    
    typeof self.counter == 'undefined' && (self.counter = 0);
    typeof self.expire  == 'undefined' && (self.expire  = T_MINUTE * 15);
    
    var context    = context || window,
        persist    = persist || false,
        uniqueName = 'callback_' + ++self.counter;
    
    if (!argumentsLenth) // display settings?
    {
        log(L_ALWAYS, 'window.linkCallback', 'settings',
        {
            'counter': self.counter,
            'expire' : self.expire
        });
        
        return false;
    }
    
    // wrapper
    
    log(L_DEBUG, 'window.linkCallback', 'linking ' + uniqueName + ' (persist: ' + persist + ')');
    
    window[uniqueName] = function ()
    {
        //log(L_DEBUG, 'window.' + uniqueName, persist ? 'persisting' : 'unlinking', arguments);
        
        var self = arguments.callee;
        
        callback      && callback.apply(context, arguments);
        deferred      && deferred.resolve.apply(deferred, arguments);
        !self.persist && unlinkCallback(uniqueName);
    };
    
    // wrapper static
    
    window[uniqueName].callback = callback;
    window[uniqueName].persist  = persist;
    window[uniqueName].timeout  = persist ? null : setTimeout(function ()
    {
        log(L_DEBUG, 'window.' + uniqueName, 'auto expire') && unlinkCallback(uniqueName);
    },
    self.expire);
    
    return uniqueName;
}



/** @function log
    @desc     Send stringified values to console or file for debugging or analytics.
              Will always output regardless of verbosity and squelch settings.
    
    @arg      {...Mixed} value - Any number of any value.
    @returns  {Boolean}          true
    
    @property {Object}         verbosity         - Range of message levels to output.
    @property {Number}         verbosity.min     - Minimum depth of message levels to log, set with logging constants.
    @property {Number}         verbosity.max     - Maximum depth of message levels to log, set with logging constants.
    @property {Object}         squelch           - Range of message levels to {@link deval} arguments/html/xml.
    @property {Number}         squelch.min       - Minimum depth of message levels to pass squelch,
                                                   set with logging constants.
    @property {Number}         squelch.max       - Maximum depth of message levels to pass squelch,
                                                   set with logging constants.
    @property {Object}         truncate          - Max length of stringified arguments or output, 0 = unlimited.
    @property {Number}         truncate.argument - Max length of each argument logged.
    @property {Number}         truncate.output   - Max length of total output.
    @property {Boolean}        compress          - Whether to LZ compress data portion of log line, requires LZString.
    @property {Array.<String>} names             - Custom identifiers for message levels,
                                                   pad to same length to align log lines.
    @property {String}         separator         - Characters to delineate values within a line.
    @property {Boolean}        console           - Whether to redirect messages to the console instead of file.
    @property {Boolean}        ms                - Whether to display millisecond timestamp.
    @property {Number}         msPad             - How many places to pad timestamps.
    @property {Object}         timer             - Timer instance for generating timestamps.
    @property {String}         identifier        - String used to indicate which WebView emitted a message, defaults to
                                                   {@link Starlet.identifier} or 'connection'.
    @property {Number}         leaderLength      - Used to adaptively pad the leader of each line to align data into
                                                   columns for readability.
    @property {Array.<RegExp>} exclude           - List of patterns to test on final output, logs nothing if any match.
    
    @see      {@link log(2)}
    @see      {@link deval}
*/
/** @function log(2)
    @desc     Send stringified values to console or file for debugging or analytics.
    
    @arg      {Number}   level - Message level of line, use logging constants.
    @arg      {...Mixed} value - Any number of any value.
    @returns  {Boolean}          true
    
    @property {Object}         verbosity         - Range of message levels to output.
    @property {Number}         verbosity.min     - Minimum depth of message levels to log, set with logging constants.
    @property {Number}         verbosity.max     - Maximum depth of message levels to log, set with logging constants.
    @property {Object}         squelch           - Range of message levels to {@link deval} arguments/html/xml.
    @property {Number}         squelch.min       - Minimum depth of message levels to pass squelch,
                                                   set with logging constants.
    @property {Number}         squelch.max       - Maximum depth of message levels to pass squelch,
                                                   set with logging constants.
    @property {Object}         truncate          - Max length of stringified arguments or output, 0 = unlimited.
    @property {Number}         truncate.argument - Max length of each argument logged.
    @property {Number}         truncate.output   - Max length of total output.
    @property {Boolean}        compress          - Whether to LZ compress data portion of log line, requires LZString.
    @property {Array.<String>} names             - Custom identifiers for message levels,
                                                   pad to same length to align log lines.
    @property {String}         separator         - Characters to delineate values within a line.
    @property {Boolean}        console           - Whether to redirect messages to the console instead of file.
    @property {Boolean}        ms                - Whether to display millisecond timestamp.
    @property {Number}         msPad             - How many places to pad timestamps.
    @property {Object}         timer             - Timer instance for generating timestamps.
    @property {String}         identifier        - String used to indicate which WebView emitted a message, defaults to
                                                   {@link Starlet.identifier} or 'connection'.
    @property {Number}         leaderLength      - Used to adaptively pad the leader of each line to align data into
                                                   columns for readability.
    @property {Array.<RegExp>} exclude           - List of patterns to test on final output, logs nothing if any match.
    
    @see      {@link log}
    @see      {@link deval}
*/
function log()
{
    var self            = arguments.callee,
        argumentsLength = arguments.length,
        level           = 0,
        type            = '',
        message         = [],
        fwLevelsText    = ['DEBUG', 'INFO', 'WARN', 'ERROR'],
        fwLevels        =
                        /*  framework
                        
                            d   i   w   e
                            e   n   a   r
                            b   f   r   r
                            u   o   n   o
                            g           r   */
                        [
                                1,          // always   s
                                        3,  // error    c
                                    2,      // warn     r
                                1,          // main     i
                                1,          // func     p
                                1,          // info     t
                            0               // debug    
                        ];
    
    // static
    
    !exists(self.verbosity   ) && (self.verbosity    = {'min'     : L_ALWAYS, 'max'   : L_MAIN});
    !exists(self.squelch     ) && (self.squelch      = {'min'     : L_ALWAYS, 'max'   : L_MAIN});
    !exists(self.truncate    ) && (self.truncate     = {'argument': 0       , 'output': 300});
    !exists(self.compress    ) && (self.compress     = false);
    !exists(self.names       ) && (self.names        = ['always  ', 'error   ', 'warn    ', 'main    ', 'function', 'info    ', 'debug   ']);
    !exists(self.separator   ) && (self.separator    = ', ');
    !exists(self.console     ) && (self.console      = false);
    !exists(self.ms          ) && (self.ms           = false);
    !exists(self.msPad       ) && (self.msPad        = (T_MONTH * 12).toString().length);
    !exists(self.timer       ) && (self.timer        = new TimerClass());
    !exists(self.identifier  ) && (self.identifier   = pad(typeof Starlet !== 'undefined' ? Starlet.identifier : 'connection', 20));
    !exists(self.leaderLength) && (self.leaderLength = 'yyyymmdd hh:mm:ss.mmmuuu (DEBUG) : [{0}] : '.format
                                                                   (
                                                                       typeof Starlet !== 'undefined'
                                                                           ? Starlet.name
                                                                           : 'connection_starlet'
                                                                   )
                                                                   .length
                                  );
    !exists(self.exclude     ) && (self.exclude      = []);
    
    // each arg
    
    for (var i = 0; i < argumentsLength; i++)
    {
        var arg = arguments[i];
        
        // first arg is message level?
        
        if (!i && typeof arg === 'number')
        {
            level = arg;
            
            if (level >= self.verbosity.min && level <= self.verbosity.max) // in range
            {
                var text = [];
                
                //text.push(self.identifier);                                       // source webview
                self.ms && text.push(pad(self.timer.elapsed(), self.msPad, '0')); // time since first call
                text.push(self.names[level]);                       // message level
                message.push('[' + text.join(':') + ']');                         // final info string
                continue;
            }
            else { return true; /* courtesy */ } // not in range
        }
        
        // squelch appropriately
        
        switch (type = dltypeof(arg))
        {
            case 'array'             : case 'boolean'           : case 'date'              : case 'domcollection'     :
            case 'error'             : case 'funciton'          : case 'jsobject'          : case 'math'              :
            case 'mimetypecollection': case 'null'              : case 'number'            : case 'object'            :
            case 'plugincollection'  : case 'regexp'            : case 'string'            : case 'textnode'          :
            case 'textrange'         : case 'undefined'         : case 'window'            : break;
                                       
            case 'arguments'         :
            case 'domelement'        : if (level < self.squelch.min || level > self.squelch.max) { message.push('/* ' + type + ' */'); continue; } break;
                                       
            default                  : if (/^(html|element)/i.test(type)) // html-like?
                                       {
                                           if (level < self.squelch.min || level > self.squelch.max) { message.push('/* ' + type + ' */'); continue; } break;
                                       }
                                       else { break; }
        }
        
        // finalize arg
        
        arg = deval(arg);
        
        self.truncate.argument
            && arg.length > self.truncate.argument
            && (arg.substr(0, self.truncate.argument - 4) + ' ...');
        
        message.push(arg);
    }
    
    // no args? stop
    
    if (!message.length) { return true; /* courtesy */ }
    
    // finalize output
    
    message = message.join(self.separator);
    
    self.truncate.output
        && message.length + self.leaderLength + 4 > self.truncate.output
        && (message = message.substr(0, self.truncate.output - self.leaderLength - 4) + ' ...');
    
    // exclusion
    
    for (var i = 0; i < self.exclude.length; i++)
    {
        if (self.exclude[i].test(message)) { return true; }
    }
    
    // compress?
    
    if (self.compress && window.LZString)
    {
        var startLength = message.length,
            startTime   = (new Date()).getTime();
        
        message = LZString.compressToUTF16(message);
        
        var endTime   = (new Date()).getTime(),
            endLength = message.length;
        
        message = '{0}% {1}ms ~ {2}'.format(Math.round(endLength / startLength * 100), endTime - startTime, message);
    }
    
    // output
    
    ISORION && !self.console
        ? OrionSystem.logToConsole(fwLevels[level], message + "\n")
        : console.log(message);
    
    //Bus.ask('logs', {'action': 'create', 'message': message});
    
    return true; // courtesy
}



/** @function pad
    @readonly
    @desc     Prepends characters to a value.
    
    @arg      {Number|String} value     - Item to pad.
    @arg      {Number}        places    - Desired minimum length.
    @arg      {String}        character - Item to pad with.
    @returns  {String}                    Padded string of length >= places.
*/
function pad(value, places, character)
{
    value     = value.toString();
    character = character || ' ';
    
    while (value.length < places) { value = character + value; }
    return value;
}



// in development
function sanitizeJSON(input)
{
    var self   = arguments.callee,
        output = null;
    
    switch (dltypeof(input))
    {
        case 'arguments': input = [].slice.call(input);
        case 'object'   :
        case 'jsobject' :
            
            output   = {},
            returned = null;
            
            for (var key in input)
            {
                input.hasOwnProperty(key)
                    && (returned    = self(input[key])) !== null
                    && (output[key] = returned);
            }
            //console.log('obj', output);
            break;
            
        case 'array':
            
            output = [];
            
            for (var index = 0; index < input.length; index++)
            {
                (returned = self(input[index])) !== null
                    && (output[index] = returned);
            }
            //console.log('obj', output);
            break;
            
        case 'string' :
        case 'number' :
        case 'boolean':
        case 'null'   : output = input; break;
            
        default: break;
    }
    
    return output || null;
}



/** @function sanitizeParams
    @readonly
    @desc     Converts properties to strings for safer assembly of URLs.
    
    @arg      {Object} params - Parameters object to sanitize.
    @returns  {Object}          Sanitized parameters object
*/
function sanitizeParams(params)
{
    for (var key in params)
    {
        typeof params[key] === 'number' && (params[key] = String(params[key]));
    };
    
    return params;
}



/** @function touch
    @readonly
    @desc     Creates a property on an object if it doesn't exist, defaults to value given, and returns the reference.
    
    @arg      {Object} obj      - Container object.
    @arg      {String} property - Key to create if undefined.
    @arg      {Mixed}  [value]  - Item to set key to, otherwise null.
    @returns  {Mixed}             Reference to obj.property.
*/
function touch(obj, property, value)
{
    !obj.hasOwnProperty(property) && (obj[property] = value || null);
    
    return obj[property];
}



/** @function unlinkCallback
    @readonly
    @desc     Removes a callback wrapper generated with {@link linkCallback}.
    
    @arg      {String|Function} callback - Generated function name or original callback.
    @returns  {Boolean}                    Success or failure.
    
    @seec     {@link linkCallback}
*/
function unlinkCallback(callback)
{
    switch (typeof callback)
    {
        case 'string':
            
            log(L_DEBUG, 'window.unlinkCallback', callback, 'by name');
            clearTimeout(window[callback].timeout);
            delete window[callback];
            return true;
            
        case 'function':
            
            var pattern = new RegExp('^callback_');
            
            for (key in window)
            {
                if (pattern.test(key) && window[key] === callback)
                {
                    log(L_DEBUG, 'window.unlinkCallback', key, 'by reference');
                    clearTimeout(window[key].timeout);
                    delete window[key];
                    return true;
                }
            }
            
        default: return false;
    }
}