// Underscore.js 1.4.4
// ===================

// > http://underscorejs.org
// > (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc.
// > Underscore may be freely distributed under the MIT license.

// Baseline setup
// --------------
(function() {

  // Establish the root object, `window` in the browser, or `global` on the server.
  var root = this;

  // Save the previous value of the `_` variable.
  var previousUnderscore = root._;

  // Establish the object that gets returned to break out of a loop iteration.
  var breaker = {};

  // Save bytes in the minified (but not gzipped) version:
  var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;

  // Create quick reference variables for speed access to core prototypes.
  var push             = ArrayProto.push,
      slice            = ArrayProto.slice,
      concat           = ArrayProto.concat,
      toString         = ObjProto.toString,
      hasOwnProperty   = ObjProto.hasOwnProperty;

  // All **ECMAScript 5** native function implementations that we hope to use
  // are declared here.
  var
    nativeForEach      = ArrayProto.forEach,
    nativeMap          = ArrayProto.map,
    nativeReduce       = ArrayProto.reduce,
    nativeReduceRight  = ArrayProto.reduceRight,
    nativeFilter       = ArrayProto.filter,
    nativeEvery        = ArrayProto.every,
    nativeSome         = ArrayProto.some,
    nativeIndexOf      = ArrayProto.indexOf,
    nativeLastIndexOf  = ArrayProto.lastIndexOf,
    nativeIsArray      = Array.isArray,
    nativeKeys         = Object.keys,
    nativeBind         = FuncProto.bind;

  // Create a safe reference to the Underscore object for use below.
  var _ = function(obj) {
    if (obj instanceof _) return obj;
    if (!(this instanceof _)) return new _(obj);
    this._wrapped = obj;
  };

  // Export the Underscore object for **Node.js**, with
  // backwards-compatibility for the old `require()` API. If we're in
  // the browser, add `_` as a global object via a string identifier,
  // for Closure Compiler "advanced" mode.
  if (typeof exports !== 'undefined') {
    if (typeof module !== 'undefined' && module.exports) {
      exports = module.exports = _;
    }
    exports._ = _;
  } else {
    root._ = _;
  }

  // Current version.
  _.VERSION = '1.4.4';

  // Collection Functions
  // --------------------

  // The cornerstone, an `each` implementation, aka `forEach`.
  // Handles objects with the built-in `forEach`, arrays, and raw objects.
  // Delegates to **ECMAScript 5**'s native `forEach` if available.
  var each = _.each = _.forEach = function(obj, iterator, context) {
    if (obj == null) return;
    if (nativeForEach && obj.forEach === nativeForEach) {
      obj.forEach(iterator, context);
    } else if (obj.length === +obj.length) {
      for (var i = 0, l = obj.length; i < l; i++) {
        if (iterator.call(context, obj[i], i, obj) === breaker) return;
      }
    } else {
      for (var key in obj) {
        if (_.has(obj, key)) {
          if (iterator.call(context, obj[key], key, obj) === breaker) return;
        }
      }
    }
  };

  // Return the results of applying the iterator to each element.
  // Delegates to **ECMAScript 5**'s native `map` if available.
  _.map = _.collect = function(obj, iterator, context) {
    var results = [];
    if (obj == null) return results;
    if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
    each(obj, function(value, index, list) {
      results[results.length] = iterator.call(context, value, index, list);
    });
    return results;
  };

  var reduceError = 'Reduce of empty array with no initial value';

  // **Reduce** builds up a single result from a list of values, aka `inject`,
  // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
  _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
    var initial = arguments.length > 2;
    if (obj == null) obj = [];
    if (nativeReduce && obj.reduce === nativeReduce) {
      if (context) iterator = _.bind(iterator, context);
      return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
    }
    each(obj, function(value, index, list) {
      if (!initial) {
        memo = value;
        initial = true;
      } else {
        memo = iterator.call(context, memo, value, index, list);
      }
    });
    if (!initial) throw new TypeError(reduceError);
    return memo;
  };

  // The right-associative version of reduce, also known as `foldr`.
  // Delegates to **ECMAScript 5**'s native `reduceRight` if available.
  _.reduceRight = _.foldr = function(obj, iterator, memo, context) {
    var initial = arguments.length > 2;
    if (obj == null) obj = [];
    if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
      if (context) iterator = _.bind(iterator, context);
      return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
    }
    var length = obj.length;
    if (length !== +length) {
      var keys = _.keys(obj);
      length = keys.length;
    }
    each(obj, function(value, index, list) {
      index = keys ? keys[--length] : --length;
      if (!initial) {
        memo = obj[index];
        initial = true;
      } else {
        memo = iterator.call(context, memo, obj[index], index, list);
      }
    });
    if (!initial) throw new TypeError(reduceError);
    return memo;
  };

  // Return the first value which passes a truth test. Aliased as `detect`.
  _.find = _.detect = function(obj, iterator, context) {
    var result;
    any(obj, function(value, index, list) {
      if (iterator.call(context, value, index, list)) {
        result = value;
        return true;
      }
    });
    return result;
  };

  // Return all the elements that pass a truth test.
  // Delegates to **ECMAScript 5**'s native `filter` if available.
  // Aliased as `select`.
  _.filter = _.select = function(obj, iterator, context) {
    var results = [];
    if (obj == null) return results;
    if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
    each(obj, function(value, index, list) {
      if (iterator.call(context, value, index, list)) results[results.length] = value;
    });
    return results;
  };

  // Return all the elements for which a truth test fails.
  _.reject = function(obj, iterator, context) {
    return _.filter(obj, function(value, index, list) {
      return !iterator.call(context, value, index, list);
    }, context);
  };

  // Determine whether all of the elements match a truth test.
  // Delegates to **ECMAScript 5**'s native `every` if available.
  // Aliased as `all`.
  _.every = _.all = function(obj, iterator, context) {
    iterator || (iterator = _.identity);
    var result = true;
    if (obj == null) return result;
    if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
    each(obj, function(value, index, list) {
      if (!(result = result && iterator.call(context, value, index, list))) return breaker;
    });
    return !!result;
  };

  // Determine if at least one element in the object matches a truth test.
  // Delegates to **ECMAScript 5**'s native `some` if available.
  // Aliased as `any`.
  var any = _.some = _.any = function(obj, iterator, context) {
    iterator || (iterator = _.identity);
    var result = false;
    if (obj == null) return result;
    if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
    each(obj, function(value, index, list) {
      if (result || (result = iterator.call(context, value, index, list))) return breaker;
    });
    return !!result;
  };

  // Determine if the array or object contains a given value (using `===`).
  // Aliased as `include`.
  _.contains = _.include = function(obj, target) {
    if (obj == null) return false;
    if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
    return any(obj, function(value) {
      return value === target;
    });
  };

  // Invoke a method (with arguments) on every item in a collection.
  _.invoke = function(obj, method) {
    var args = slice.call(arguments, 2);
    var isFunc = _.isFunction(method);
    return _.map(obj, function(value) {
      return (isFunc ? method : value[method]).apply(value, args);
    });
  };

  // Convenience version of a common use case of `map`: fetching a property.
  _.pluck = function(obj, key) {
    return _.map(obj, function(value){ return value[key]; });
  };

  // Convenience version of a common use case of `filter`: selecting only objects
  // containing specific `key:value` pairs.
  _.where = function(obj, attrs, first) {
    if (_.isEmpty(attrs)) return first ? null : [];
    return _[first ? 'find' : 'filter'](obj, function(value) {
      for (var key in attrs) {
        if (attrs[key] !== value[key]) return false;
      }
      return true;
    });
  };

  // Convenience version of a common use case of `find`: getting the first object
  // containing specific `key:value` pairs.
  _.findWhere = function(obj, attrs) {
    return _.where(obj, attrs, true);
  };

  // Return the maximum element or (element-based computation).
  // Can't optimize arrays of integers longer than 65,535 elements.
  // See: https://bugs.webkit.org/show_bug.cgi?id=80797
  _.max = function(obj, iterator, context) {
    if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
      return Math.max.apply(Math, obj);
    }
    if (!iterator && _.isEmpty(obj)) return -Infinity;
    var result = {computed : -Infinity, value: -Infinity};
    each(obj, function(value, index, list) {
      var computed = iterator ? iterator.call(context, value, index, list) : value;
      computed >= result.computed && (result = {value : value, computed : computed});
    });
    return result.value;
  };

  // Return the minimum element (or element-based computation).
  _.min = function(obj, iterator, context) {
    if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
      return Math.min.apply(Math, obj);
    }
    if (!iterator && _.isEmpty(obj)) return Infinity;
    var result = {computed : Infinity, value: Infinity};
    each(obj, function(value, index, list) {
      var computed = iterator ? iterator.call(context, value, index, list) : value;
      computed < result.computed && (result = {value : value, computed : computed});
    });
    return result.value;
  };

  // Shuffle an array.
  _.shuffle = function(obj) {
    var rand;
    var index = 0;
    var shuffled = [];
    each(obj, function(value) {
      rand = _.random(index++);
      shuffled[index - 1] = shuffled[rand];
      shuffled[rand] = value;
    });
    return shuffled;
  };

  // An internal function to generate lookup iterators.
  var lookupIterator = function(value) {
    return _.isFunction(value) ? value : function(obj){ return obj[value]; };
  };

  // Sort the object's values by a criterion produced by an iterator.
  _.sortBy = function(obj, value, context) {
    var iterator = lookupIterator(value);
    return _.pluck(_.map(obj, function(value, index, list) {
      return {
        value : value,
        index : index,
        criteria : iterator.call(context, value, index, list)
      };
    }).sort(function(left, right) {
      var a = left.criteria;
      var b = right.criteria;
      if (a !== b) {
        if (a > b || a === void 0) return 1;
        if (a < b || b === void 0) return -1;
      }
      return left.index < right.index ? -1 : 1;
    }), 'value');
  };

  // An internal function used for aggregate "group by" operations.
  var group = function(obj, value, context, behavior) {
    var result = {};
    var iterator = lookupIterator(value || _.identity);
    each(obj, function(value, index) {
      var key = iterator.call(context, value, index, obj);
      behavior(result, key, value);
    });
    return result;
  };

  // Groups the object's values by a criterion. Pass either a string attribute
  // to group by, or a function that returns the criterion.
  _.groupBy = function(obj, value, context) {
    return group(obj, value, context, function(result, key, value) {
      (_.has(result, key) ? result[key] : (result[key] = [])).push(value);
    });
  };

  // Counts instances of an object that group by a certain criterion. Pass
  // either a string attribute to count by, or a function that returns the
  // criterion.
  _.countBy = function(obj, value, context) {
    return group(obj, value, context, function(result, key) {
      if (!_.has(result, key)) result[key] = 0;
      result[key]++;
    });
  };

  // Use a comparator function to figure out the smallest index at which
  // an object should be inserted so as to maintain order. Uses binary search.
  _.sortedIndex = function(array, obj, iterator, context) {
    iterator = iterator == null ? _.identity : lookupIterator(iterator);
    var value = iterator.call(context, obj);
    var low = 0, high = array.length;
    while (low < high) {
      var mid = (low + high) >>> 1;
      iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid;
    }
    return low;
  };

  // Safely convert anything iterable into a real, live array.
  _.toArray = function(obj) {
    if (!obj) return [];
    if (_.isArray(obj)) return slice.call(obj);
    if (obj.length === +obj.length) return _.map(obj, _.identity);
    return _.values(obj);
  };

  // Return the number of elements in an object.
  _.size = function(obj) {
    if (obj == null) return 0;
    return (obj.length === +obj.length) ? obj.length : _.keys(obj).length;
  };

  // Array Functions
  // ---------------

  // Get the first element of an array. Passing **n** will return the first N
  // values in the array. Aliased as `head` and `take`. The **guard** check
  // allows it to work with `_.map`.
  _.first = _.head = _.take = function(array, n, guard) {
    if (array == null) return void 0;
    return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
  };

  // Returns everything but the last entry of the array. Especially useful on
  // the arguments object. Passing **n** will return all the values in
  // the array, excluding the last N. The **guard** check allows it to work with
  // `_.map`.
  _.initial = function(array, n, guard) {
    return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
  };

  // Get the last element of an array. Passing **n** will return the last N
  // values in the array. The **guard** check allows it to work with `_.map`.
  _.last = function(array, n, guard) {
    if (array == null) return void 0;
    if ((n != null) && !guard) {
      return slice.call(array, Math.max(array.length - n, 0));
    } else {
      return array[array.length - 1];
    }
  };

  // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
  // Especially useful on the arguments object. Passing an **n** will return
  // the rest N values in the array. The **guard**
  // check allows it to work with `_.map`.
  _.rest = _.tail = _.drop = function(array, n, guard) {
    return slice.call(array, (n == null) || guard ? 1 : n);
  };

  // Trim out all falsy values from an array.
  _.compact = function(array) {
    return _.filter(array, _.identity);
  };

  // Internal implementation of a recursive `flatten` function.
  var flatten = function(input, shallow, output) {
    each(input, function(value) {
      if (_.isArray(value)) {
        shallow ? push.apply(output, value) : flatten(value, shallow, output);
      } else {
        output.push(value);
      }
    });
    return output;
  };

  // Return a completely flattened version of an array.
  _.flatten = function(array, shallow) {
    return flatten(array, shallow, []);
  };

  // Return a version of the array that does not contain the specified value(s).
  _.without = function(array) {
    return _.difference(array, slice.call(arguments, 1));
  };

  // Produce a duplicate-free version of the array. If the array has already
  // been sorted, you have the option of using a faster algorithm.
  // Aliased as `unique`.
  _.uniq = _.unique = function(array, isSorted, iterator, context) {
    if (_.isFunction(isSorted)) {
      context = iterator;
      iterator = isSorted;
      isSorted = false;
    }
    var initial = iterator ? _.map(array, iterator, context) : array;
    var results = [];
    var seen = [];
    each(initial, function(value, index) {
      if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) {
        seen.push(value);
        results.push(array[index]);
      }
    });
    return results;
  };

  // Produce an array that contains the union: each distinct element from all of
  // the passed-in arrays.
  _.union = function() {
    return _.uniq(concat.apply(ArrayProto, arguments));
  };

  // Produce an array that contains every item shared between all the
  // passed-in arrays.
  _.intersection = function(array) {
    var rest = slice.call(arguments, 1);
    return _.filter(_.uniq(array), function(item) {
      return _.every(rest, function(other) {
        return _.indexOf(other, item) >= 0;
      });
    });
  };

  // Take the difference between one array and a number of other arrays.
  // Only the elements present in just the first array will remain.
  _.difference = function(array) {
    var rest = concat.apply(ArrayProto, slice.call(arguments, 1));
    return _.filter(array, function(value){ return !_.contains(rest, value); });
  };

  // Zip together multiple lists into a single array -- elements that share
  // an index go together.
  _.zip = function() {
    var args = slice.call(arguments);
    var length = _.max(_.pluck(args, 'length'));
    var results = new Array(length);
    for (var i = 0; i < length; i++) {
      results[i] = _.pluck(args, "" + i);
    }
    return results;
  };

  // Converts lists into objects. Pass either a single array of `[key, value]`
  // pairs, or two parallel arrays of the same length -- one of keys, and one of
  // the corresponding values.
  _.object = function(list, values) {
    if (list == null) return {};
    var result = {};
    for (var i = 0, l = list.length; i < l; i++) {
      if (values) {
        result[list[i]] = values[i];
      } else {
        result[list[i][0]] = list[i][1];
      }
    }
    return result;
  };

  // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
  // we need this function. Return the position of the first occurrence of an
  // item in an array, or -1 if the item is not included in the array.
  // Delegates to **ECMAScript 5**'s native `indexOf` if available.
  // If the array is large and already in sort order, pass `true`
  // for **isSorted** to use binary search.
  _.indexOf = function(array, item, isSorted) {
    if (array == null) return -1;
    var i = 0, l = array.length;
    if (isSorted) {
      if (typeof isSorted == 'number') {
        i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted);
      } else {
        i = _.sortedIndex(array, item);
        return array[i] === item ? i : -1;
      }
    }
    if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted);
    for (; i < l; i++) if (array[i] === item) return i;
    return -1;
  };

  // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
  _.lastIndexOf = function(array, item, from) {
    if (array == null) return -1;
    var hasIndex = from != null;
    if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) {
      return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item);
    }
    var i = (hasIndex ? from : array.length);
    while (i--) if (array[i] === item) return i;
    return -1;
  };

  // Generate an integer Array containing an arithmetic progression. A port of
  // the native Python `range()` function. See
  // [the Python documentation](http://docs.python.org/library/functions.html#range).
  _.range = function(start, stop, step) {
    if (arguments.length <= 1) {
      stop = start || 0;
      start = 0;
    }
    step = arguments[2] || 1;

    var len = Math.max(Math.ceil((stop - start) / step), 0);
    var idx = 0;
    var range = new Array(len);

    while(idx < len) {
      range[idx++] = start;
      start += step;
    }

    return range;
  };

  // Function (ahem) Functions
  // ------------------

  // Create a function bound to a given object (assigning `this`, and arguments,
  // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
  // available.
  _.bind = function(func, context) {
    if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
    var args = slice.call(arguments, 2);
    return function() {
      return func.apply(context, args.concat(slice.call(arguments)));
    };
  };

  // Partially apply a function by creating a version that has had some of its
  // arguments pre-filled, without changing its dynamic `this` context.
  _.partial = function(func) {
    var args = slice.call(arguments, 1);
    return function() {
      return func.apply(this, args.concat(slice.call(arguments)));
    };
  };

  // Bind all of an object's methods to that object. Useful for ensuring that
  // all callbacks defined on an object belong to it.
  _.bindAll = function(obj) {
    var funcs = slice.call(arguments, 1);
    if (funcs.length === 0) funcs = _.functions(obj);
    each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
    return obj;
  };

  // Memoize an expensive function by storing its results.
  _.memoize = function(func, hasher) {
    var memo = {};
    hasher || (hasher = _.identity);
    return function() {
      var key = hasher.apply(this, arguments);
      return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
    };
  };

  // Delays a function for the given number of milliseconds, and then calls
  // it with the arguments supplied.
  _.delay = function(func, wait) {
    var args = slice.call(arguments, 2);
    return setTimeout(function(){ return func.apply(null, args); }, wait);
  };

  // Defers a function, scheduling it to run after the current call stack has
  // cleared.
  _.defer = function(func) {
    return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
  };

  // Returns a function, that, when invoked, will only be triggered at most once
  // during a given window of time.
  _.throttle = function(func, wait) {
    var context, args, timeout, result;
    var previous = 0;
    var later = function() {
      previous = new Date;
      timeout = null;
      result = func.apply(context, args);
    };
    return function() {
      var now = new Date;
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      if (remaining <= 0) {
        clearTimeout(timeout);
        timeout = null;
        previous = now;
        result = func.apply(context, args);
      } else if (!timeout) {
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
  };

  // Returns a function, that, as long as it continues to be invoked, will not
  // be triggered. The function will be called after it stops being called for
  // N milliseconds. If `immediate` is passed, trigger the function on the
  // leading edge, instead of the trailing.
  _.debounce = function(func, wait, immediate) {
    var timeout, result;
    return function() {
      var context = this, args = arguments;
      var later = function() {
        timeout = null;
        if (!immediate) result = func.apply(context, args);
      };
      var callNow = immediate && !timeout;
      clearTimeout(timeout);
      timeout = setTimeout(later, wait);
      if (callNow) result = func.apply(context, args);
      return result;
    };
  };

  // Returns a function that will be executed at most one time, no matter how
  // often you call it. Useful for lazy initialization.
  _.once = function(func) {
    var ran = false, memo;
    return function() {
      if (ran) return memo;
      ran = true;
      memo = func.apply(this, arguments);
      func = null;
      return memo;
    };
  };

  // Returns the first function passed as an argument to the second,
  // allowing you to adjust arguments, run code before and after, and
  // conditionally execute the original function.
  _.wrap = function(func, wrapper) {
    return function() {
      var args = [func];
      push.apply(args, arguments);
      return wrapper.apply(this, args);
    };
  };

  // Returns a function that is the composition of a list of functions, each
  // consuming the return value of the function that follows.
  _.compose = function() {
    var funcs = arguments;
    return function() {
      var args = arguments;
      for (var i = funcs.length - 1; i >= 0; i--) {
        args = [funcs[i].apply(this, args)];
      }
      return args[0];
    };
  };

  // Returns a function that will only be executed after being called N times.
  _.after = function(times, func) {
    if (times <= 0) return func();
    return function() {
      if (--times < 1) {
        return func.apply(this, arguments);
      }
    };
  };

  // Object Functions
  // ----------------

  // Retrieve the names of an object's properties.
  // Delegates to **ECMAScript 5**'s native `Object.keys`
  _.keys = nativeKeys || function(obj) {
    if (obj !== Object(obj)) throw new TypeError('Invalid object');
    var keys = [];
    for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key;
    return keys;
  };

  // Retrieve the values of an object's properties.
  _.values = function(obj) {
    var values = [];
    for (var key in obj) if (_.has(obj, key)) values.push(obj[key]);
    return values;
  };

  // Convert an object into a list of `[key, value]` pairs.
  _.pairs = function(obj) {
    var pairs = [];
    for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]);
    return pairs;
  };

  // Invert the keys and values of an object. The values must be serializable.
  _.invert = function(obj) {
    var result = {};
    for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key;
    return result;
  };

  // Return a sorted list of the function names available on the object.
  // Aliased as `methods`
  _.functions = _.methods = function(obj) {
    var names = [];
    for (var key in obj) {
      if (_.isFunction(obj[key])) names.push(key);
    }
    return names.sort();
  };

  // Extend a given object with all the properties in passed-in object(s).
  _.extend = function(obj) {
    each(slice.call(arguments, 1), function(source) {
      if (source) {
        for (var prop in source) {
          obj[prop] = source[prop];
        }
      }
    });
    return obj;
  };

  // Return a copy of the object only containing the whitelisted properties.
  _.pick = function(obj) {
    var copy = {};
    var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
    each(keys, function(key) {
      if (key in obj) copy[key] = obj[key];
    });
    return copy;
  };

   // Return a copy of the object without the blacklisted properties.
  _.omit = function(obj) {
    var copy = {};
    var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
    for (var key in obj) {
      if (!_.contains(keys, key)) copy[key] = obj[key];
    }
    return copy;
  };

  // Fill in a given object with default properties.
  _.defaults = function(obj) {
    each(slice.call(arguments, 1), function(source) {
      if (source) {
        for (var prop in source) {
          if (obj[prop] == null) obj[prop] = source[prop];
        }
      }
    });
    return obj;
  };

  // Create a (shallow-cloned) duplicate of an object.
  _.clone = function(obj) {
    if (!_.isObject(obj)) return obj;
    return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
  };

  // Invokes interceptor with the obj, and then returns obj.
  // The primary purpose of this method is to "tap into" a method chain, in
  // order to perform operations on intermediate results within the chain.
  _.tap = function(obj, interceptor) {
    interceptor(obj);
    return obj;
  };

  // Internal recursive comparison function for `isEqual`.
  var eq = function(a, b, aStack, bStack) {
    // Identical objects are equal. `0 === -0`, but they aren't identical.
    // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.
    if (a === b) return a !== 0 || 1 / a == 1 / b;
    // A strict comparison is necessary because `null == undefined`.
    if (a == null || b == null) return a === b;
    // Unwrap any wrapped objects.
    if (a instanceof _) a = a._wrapped;
    if (b instanceof _) b = b._wrapped;
    // Compare `[[Class]]` names.
    var className = toString.call(a);
    if (className != toString.call(b)) return false;
    switch (className) {
      // Strings, numbers, dates, and booleans are compared by value.
      case '[object String]':
        // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
        // equivalent to `new String("5")`.
        return a == String(b);
      case '[object Number]':
        // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
        // other numeric values.
        return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
      case '[object Date]':
      case '[object Boolean]':
        // Coerce dates and booleans to numeric primitive values. Dates are compared by their
        // millisecond representations. Note that invalid dates with millisecond representations
        // of `NaN` are not equivalent.
        return +a == +b;
      // RegExps are compared by their source patterns and flags.
      case '[object RegExp]':
        return a.source == b.source &&
               a.global == b.global &&
               a.multiline == b.multiline &&
               a.ignoreCase == b.ignoreCase;
    }
    if (typeof a != 'object' || typeof b != 'object') return false;
    // Assume equality for cyclic structures. The algorithm for detecting cyclic
    // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
    var length = aStack.length;
    while (length--) {
      // Linear search. Performance is inversely proportional to the number of
      // unique nested structures.
      if (aStack[length] == a) return bStack[length] == b;
    }
    // Add the first object to the stack of traversed objects.
    aStack.push(a);
    bStack.push(b);
    var size = 0, result = true;
    // Recursively compare objects and arrays.
    if (className == '[object Array]') {
      // Compare array lengths to determine if a deep comparison is necessary.
      size = a.length;
      result = size == b.length;
      if (result) {
        // Deep compare the contents, ignoring non-numeric properties.
        while (size--) {
          if (!(result = eq(a[size], b[size], aStack, bStack))) break;
        }
      }
    } else {
      // Objects with different constructors are not equivalent, but `Object`s
      // from different frames are.
      var aCtor = a.constructor, bCtor = b.constructor;
      if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) &&
                               _.isFunction(bCtor) && (bCtor instanceof bCtor))) {
        return false;
      }
      // Deep compare objects.
      for (var key in a) {
        if (_.has(a, key)) {
          // Count the expected number of properties.
          size++;
          // Deep compare each member.
          if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;
        }
      }
      // Ensure that both objects contain the same number of properties.
      if (result) {
        for (key in b) {
          if (_.has(b, key) && !(size--)) break;
        }
        result = !size;
      }
    }
    // Remove the first object from the stack of traversed objects.
    aStack.pop();
    bStack.pop();
    return result;
  };

  // Perform a deep comparison to check if two objects are equal.
  _.isEqual = function(a, b) {
    return eq(a, b, [], []);
  };

  // Is a given array, string, or object empty?
  // An "empty" object has no enumerable own-properties.
  _.isEmpty = function(obj) {
    if (obj == null) return true;
    if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
    for (var key in obj) if (_.has(obj, key)) return false;
    return true;
  };

  // Is a given value a DOM element?
  _.isElement = function(obj) {
    return !!(obj && obj.nodeType === 1);
  };

  // Is a given value an array?
  // Delegates to ECMA5's native Array.isArray
  _.isArray = nativeIsArray || function(obj) {
    return toString.call(obj) == '[object Array]';
  };

  // Is a given variable an object?
  _.isObject = function(obj) {
    return obj === Object(obj);
  };

  // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.
  each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {
    _['is' + name] = function(obj) {
      return toString.call(obj) == '[object ' + name + ']';
    };
  });

  // Define a fallback version of the method in browsers (ahem, IE), where
  // there isn't any inspectable "Arguments" type.
  if (!_.isArguments(arguments)) {
    _.isArguments = function(obj) {
      return !!(obj && _.has(obj, 'callee'));
    };
  }

  // Optimize `isFunction` if appropriate.
  if (typeof (/./) !== 'function') {
    _.isFunction = function(obj) {
      return typeof obj === 'function';
    };
  }

  // Is a given object a finite number?
  _.isFinite = function(obj) {
    return isFinite(obj) && !isNaN(parseFloat(obj));
  };

  // Is the given value `NaN`? (NaN is the only number which does not equal itself).
  _.isNaN = function(obj) {
    return _.isNumber(obj) && obj != +obj;
  };

  // Is a given value a boolean?
  _.isBoolean = function(obj) {
    return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
  };

  // Is a given value equal to null?
  _.isNull = function(obj) {
    return obj === null;
  };

  // Is a given variable undefined?
  _.isUndefined = function(obj) {
    return obj === void 0;
  };

  // Shortcut function for checking if an object has a given property directly
  // on itself (in other words, not on a prototype).
  _.has = function(obj, key) {
    return hasOwnProperty.call(obj, key);
  };

  // Utility Functions
  // -----------------

  // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
  // previous owner. Returns a reference to the Underscore object.
  _.noConflict = function() {
    root._ = previousUnderscore;
    return this;
  };

  // Keep the identity function around for default iterators.
  _.identity = function(value) {
    return value;
  };

  // Run a function **n** times.
  _.times = function(n, iterator, context) {
    var accum = Array(n);
    for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i);
    return accum;
  };

  // Return a random integer between min and max (inclusive).
  _.random = function(min, max) {
    if (max == null) {
      max = min;
      min = 0;
    }
    return min + Math.floor(Math.random() * (max - min + 1));
  };

  // List of HTML entities for escaping.
  var entityMap = {
    escape: {
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#x27;',
      '/': '&#x2F;'
    }
  };
  entityMap.unescape = _.invert(entityMap.escape);

  // Regexes containing the keys and values listed immediately above.
  var entityRegexes = {
    escape:   new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'),
    unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g')
  };

  // Functions for escaping and unescaping strings to/from HTML interpolation.
  _.each(['escape', 'unescape'], function(method) {
    _[method] = function(string) {
      if (string == null) return '';
      return ('' + string).replace(entityRegexes[method], function(match) {
        return entityMap[method][match];
      });
    };
  });

  // If the value of the named property is a function then invoke it;
  // otherwise, return it.
  _.result = function(object, property) {
    if (object == null) return null;
    var value = object[property];
    return _.isFunction(value) ? value.call(object) : value;
  };

  // Add your own custom functions to the Underscore object.
  _.mixin = function(obj) {
    each(_.functions(obj), function(name){
      var func = _[name] = obj[name];
      _.prototype[name] = function() {
        var args = [this._wrapped];
        push.apply(args, arguments);
        return result.call(this, func.apply(_, args));
      };
    });
  };

  // Generate a unique integer id (unique within the entire client session).
  // Useful for temporary DOM ids.
  var idCounter = 0;
  _.uniqueId = function(prefix) {
    var id = ++idCounter + '';
    return prefix ? prefix + id : id;
  };

  // By default, Underscore uses ERB-style template delimiters, change the
  // following template settings to use alternative delimiters.
  _.templateSettings = {
    evaluate    : /<%([\s\S]+?)%>/g,
    interpolate : /<%=([\s\S]+?)%>/g,
    escape      : /<%-([\s\S]+?)%>/g
  };

  // When customizing `templateSettings`, if you don't want to define an
  // interpolation, evaluation or escaping regex, we need one that is
  // guaranteed not to match.
  var noMatch = /(.)^/;

  // Certain characters need to be escaped so that they can be put into a
  // string literal.
  var escapes = {
    "'":      "'",
    '\\':     '\\',
    '\r':     'r',
    '\n':     'n',
    '\t':     't',
    '\u2028': 'u2028',
    '\u2029': 'u2029'
  };

  var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;

  // JavaScript micro-templating, similar to John Resig's implementation.
  // Underscore templating handles arbitrary delimiters, preserves whitespace,
  // and correctly escapes quotes within interpolated code.
  _.template = function(text, data, settings) {
    var render;
    settings = _.defaults({}, settings, _.templateSettings);

    // Combine delimiters into one regular expression via alternation.
    var matcher = new RegExp([
      (settings.escape || noMatch).source,
      (settings.interpolate || noMatch).source,
      (settings.evaluate || noMatch).source
    ].join('|') + '|$', 'g');

    // Compile the template source, escaping string literals appropriately.
    var index = 0;
    var source = "__p+='";
    text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
      source += text.slice(index, offset)
        .replace(escaper, function(match) { return '\\' + escapes[match]; });

      if (escape) {
        source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
      }
      if (interpolate) {
        source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
      }
      if (evaluate) {
        source += "';\n" + evaluate + "\n__p+='";
      }
      index = offset + match.length;
      return match;
    });
    source += "';\n";

    // If a variable is not specified, place data values in local scope.
    if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';

    source = "var __t,__p='',__j=Array.prototype.join," +
      "print=function(){__p+=__j.call(arguments,'');};\n" +
      source + "return __p;\n";

    try {
      render = new Function(settings.variable || 'obj', '_', source);
    } catch (e) {
      e.source = source;
      throw e;
    }

    if (data) return render(data, _);
    var template = function(data) {
      return render.call(this, data, _);
    };

    // Provide the compiled function source as a convenience for precompilation.
    template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';

    return template;
  };

  // Add a "chain" function, which will delegate to the wrapper.
  _.chain = function(obj) {
    return _(obj).chain();
  };

  // OOP
  // ---------------
  // If Underscore is called as a function, it returns a wrapped object that
  // can be used OO-style. This wrapper holds altered versions of all the
  // underscore functions. Wrapped objects may be chained.

  // Helper function to continue chaining intermediate results.
  var result = function(obj) {
    return this._chain ? _(obj).chain() : obj;
  };

  // Add all of the Underscore functions to the wrapper object.
  _.mixin(_);

  // Add all mutator Array functions to the wrapper.
  each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
    var method = ArrayProto[name];
    _.prototype[name] = function() {
      var obj = this._wrapped;
      method.apply(obj, arguments);
      if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0];
      return result.call(this, obj);
    };
  });

  // Add all accessor Array functions to the wrapper.
  each(['concat', 'join', 'slice'], function(name) {
    var method = ArrayProto[name];
    _.prototype[name] = function() {
      return result.call(this, method.apply(this._wrapped, arguments));
    };
  });

  _.extend(_.prototype, {

    // Start chaining a wrapped Underscore object.
    chain: function() {
      this._chain = true;
      return this;
    },

    // Extracts the result from a wrapped and chained object.
    value: function() {
      return this._wrapped;
    }

  });

}).call(this);


LI = (typeof(LI) !== "undefined" && LI) ? LI : {};
LI.i18n = LI.i18n || {};

//set up global t8 namespace or pick up existing one
t8 = this['t8'] ? this['t8'] : {};

//if underscore is concatenated along with t8 (for USSR rpm)
//grab _ object from module exports and make it global
if(typeof _ == 'undefined' && typeof exports !== 'undefined') {
  _ = exports._;
}
/**
 * A small set of general purpose utility functions
 */
(function(root, factory) {
  //call factory to create t8 object
  var lib = factory(t8);

  //set up as global variable for browsers, rhino and node
  root['t8'] = lib;
  //set up as export for node
  if (typeof exports !== 'undefined') {
    module.exports = lib;
  }
}(this, function(t8) {
  "use strict";

  var Utils = t8.Utils = {};

  /*******************************************************************
   * Underscore dependency removed by integrating necessary components of Underscore
   * directly into the t8.Utils framework
   *******************************************************************/
  // Establish the object that gets returned to break out of a loop iteration.
  var breaker = {};

  // Save bytes in the minified (but not gzipped) version:
  var ArrayProto      = Array.prototype, 
      ObjProto        = Object.prototype, 
      FuncProto       = Function.prototype;

  // Create quick reference variables for speed access to core prototypes.
  var
    push              = ArrayProto.push,
    slice             = ArrayProto.slice,
    concat            = ArrayProto.concat,
    toString          = ObjProto.toString,
    hasOwnProperty    = ObjProto.hasOwnProperty;

  // All **ECMAScript 5** native function implementations that we _hope_ to use
  // are declared here.
  var
    nativeForEach      = ArrayProto.forEach,
    nativeMap          = ArrayProto.map,
    nativeReduce       = ArrayProto.reduce,
    nativeReduceRight  = ArrayProto.reduceRight,
    nativeFilter       = ArrayProto.filter,
    nativeEvery        = ArrayProto.every,
    nativeSome         = ArrayProto.some,
    nativeIndexOf      = ArrayProto.indexOf,
    nativeLastIndexOf  = ArrayProto.lastIndexOf,
    nativeIsArray      = Array.isArray,
    nativeKeys         = Object.keys,
    nativeBind         = FuncProto.bind;

  // An internal function to generate lookup iterators.
  var lookupIterator = function(value) {
    return Utils.isFunction(value) ? value : function(obj){ return obj[value]; };
  };

  /**
   * Copy all of the properties in the source objects over to the destination object, and return the destination object. 
   * It's in-order, so the last source will override properties of the same name in previous arguments.
   * @param obj
   */
  Utils.extend = function(obj) {
    Utils.each(Array.prototype.slice.call(arguments, 1), function(source) {
      if (source) {
        for (var prop in source) {
          obj[prop] = source[prop];
        }
      }
    });
    return obj;
  };

  /**
   * Iterates over a list of elements, yielding each in turn to an iterator function.
   * @param obj
   * @param iterator
   * @param context
   */
  var each = Utils.each = function(obj, iterator, context) {
    if (obj == null) return;
    if (nativeForEach && obj.forEach === nativeForEach) {
      obj.forEach(iterator, context);
    } else if (obj.length === +obj.length) {
      for (var i = 0, length = obj.length; i < length; i++) {
        if (iterator.call(context, obj[i], i, obj) === breaker) return;
      }
    } else {
      var keys = Utils.keys(obj);
      for (var i = 0, length = keys.length; i < length; i++) {
        if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return;
      }
    }
  };

  /**
   * Returns true if any of the values in the list pass the iterator truth test. 
   * Short-circuits and stops traversing the list if a true element is found.
   * @param obj
   * @param iterator
   * @param context
   */
  var any = Utils.any = Utils.some = function(obj, iterator, context) {
    iterator || (iterator = Utils.identity);
    var result = false;
    if (obj == null) return result;
    if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
    Utils.each(obj, function(value, index, list) {
      if (result || (result = iterator.call(context, value, index, list))) return breaker;
    });
    return !!result;
  };

  /**
   * Looks through each value in the list, returning the first one that passes a truth test (iterator), 
   * or undefined if no value passes the test. The function returns as soon as it finds an acceptable element, 
   * and doesn't traverse the entire list.
   * @param obj
   * @param iterator
   * @param context
   */
  Utils.find = function(obj, iterator, context) {
    var result;
    any(obj, function(value, index, list) {
      if (iterator.call(context, value, index, list)) {
        result = value;
        return true;
      }
    });
    return result;
  };

  /**
   * Returns a copy of the object where the keys have become the values and the values the keys. 
   * For this to work, all of your object's values should be unique and string serializable.
   * @param obj
   */
  Utils.invert = function(obj) {
    var result = {};
    var keys = Utils.keys(obj);
    for (var i = 0, length = keys.length; i < length; i++) {
      result[obj[keys[i]]] = keys[i];
    }
    return result;
  };

  /**
   * Returns the same value that is used as the argument. In math: f(x) = x
   * @param value
   */
  Utils.identity = function(value) {
    return value;
  };

  /**
   * Shortcut function for checking if an object has a given property directly on itself (in other words, not on a prototype).
   * @param obj
   * @param key
   */
  Utils.has = function(obj, key) {
    return hasOwnProperty.call(obj, key);
  };

  /**
   * Returns true if the value is present in the list. Uses indexOf internally, if list is an Array.
   * @param obj
   * @param key
   */
  Utils.contains = function(obj, target) {
    if (obj == null) return false;
    if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
    return Utils.some(obj, function(value) {
      return value === target;
    });
  };

  /**
   * Retrieve all the names of the object's properties.
   * @param obj
   */
  Utils.keys = function(obj) {
    if (obj !== Object(obj)) throw new TypeError('Invalid object');
    var keys = [];
    for (var key in obj) if (Utils.has(obj, key)) keys.push(key);
    return keys;
  };

  /**
   * Produces a new array of values by mapping each value in list through a transformation function (iterator). 
   * If the native map method exists, it will be used instead. If list is a JavaScript object, iterator's arguments will be (value, key, list).
   * @param obj
   * @param iterator
   * @param context
   */
  Utils.map = function(obj, iterator, context) {
    var results = [];
    if (obj == null) return results;
    if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
    Utils.each(obj, function(value, index, list) {
      results.push(iterator.call(context, value, index, list));
    });
    return results;
  };

  /**
   * Returns the index at which value can be found in the array, or -1 if value is not present in the array.
   * @param array
   * @param item
   * @param isSorted
   */
  Utils.indexOf = function(array, item, isSorted) {
    if (array == null) return -1;
    var i = 0, length = array.length;
    if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted);
    for (; i < length; i++) if (array[i] === item) return i;
    return -1;
  };

  /**
   * Returns true if value is undefined.
   * @param obj
   * @returns {boolean}
   */
  Utils.isUndefined = function(obj) {
    return obj === void 0;
  };

  /**
   * Check that obj is not undefined/null
   * @param obj
   * @returns {boolean}
   */
  Utils.isDefined = function(obj) {
    return !Utils.isUndefined(obj) && !Utils.isNull(obj);
  };

  /**
   * Returns true if the value of object is null.
   * @param obj
   */
   Utils.isNull = function(obj) {
    return obj === null;
  };

  /**
   * Returns true if object is an Array.
   * @param object
   */
  Utils.isArray = function(obj) {
    return toString.call(obj) == '[object Array]';
  };

  /**
   * Returns true if object is a String.
   * @param object
   */
  Utils.isString = function(obj) {
    return toString.call(obj) == '[object String]';
  };

  /**
   * Returns true if object is a Number.
   * @param object
   */
  Utils.isNumber = function(obj) {
    return toString.call(obj) == '[object Number]';
  };

  /**
   * Returns true if object is a Function.
   * @param object
   */
  Utils.isFunction = function(obj) {
    return toString.call(obj) == '[object Function]';
  };

  /**
   * Returns true if object is Arguments.
   * @param object
   */
  Utils.isArguments = function(obj) {
    return toString.call(obj) == '[object Arguments]';
  };

  // Define a fallback version of the method in browsers (ahem, IE), where
  // there isn't any inspectable "Arguments" type.
  if (!Utils.isArguments(arguments)) {
    Utils.isArguments = function(obj) {
      return !!(obj && Utils.has(obj, 'callee'));
    };
  }

  // Optimize `isFunction` if appropriate.
  if (typeof (/./) !== 'function') {
    Utils.isFunction = function(obj) {
      return typeof obj === 'function';
    };
  }

  // List of HTML entities for escaping.
  var entityMap = {
    escape: {
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#x27;'
    }
  };
  entityMap.unescape = Utils.invert(entityMap.escape);

  // Regexes containing the keys and values listed immediately above.
  var entityRegexes = {
    escape:   new RegExp('[' + Utils.keys(entityMap.escape).join('') + ']', 'g'),
    unescape: new RegExp('(' + Utils.keys(entityMap.unescape).join('|') + ')', 'g')
  };

  // Functions for escaping and unescaping strings to/from HTML interpolation.
  Utils.each(['escape', 'unescape'], function(method) {
    Utils[method] = function(string) {
      if (string == null) return '';
      return ('' + string).replace(entityRegexes[method], function(match) {
        return entityMap[method][match];
      });
    };
  });

  /*
   * End necessary underscore components integration
   */

  /**
   * Assert that condition is true and throw an Error with message if it's not.
   * @param condition
   * @param message
   */
  Utils.assert = function(condition, message) {
    if (!condition) {
      throw new Error(message);
    }
  };

  /**
   * Assert that the given value is not undefined/null and throw an Error with message if it is
   * @param value
   * @param message
   */
  Utils.assertDefined = function(value, message) {
    Utils.assert(Utils.isDefined(value), message);
  };

  /**
   * Assert that the given value is undefined/null and throw an Error with message if it isn't
   * @param value
   * @param message
   */
  Utils.assertNotDefined = function(value, message) {
    Utils.assert(!Utils.isDefined(value), message);
  };

  /**
   * Stolen from com.linkedin.util.xmsg.XmsgUtils.doBestEffortNumberConversion
   *
   * Converts an object to a number using a not-so-best-effort approach. Strip all non-digits, including commas,
   * decimal points, and letters, and use parseInt to convert to a number. In case of any error, return 0.
   *
   * TODO: should this be in a NumberFormatter class?
   *
   * @param value
   * @returns {*}
   */
  Utils.bestEffortNumberConversion = function(value) {
    try {
      if (Utils.isDefined(value)) {
        if (Utils.isNumber(value)) {
          return value;
        }

        var digitsOnly = value.replace(/[^0-9]/g, "");
        var asInt = parseInt(digitsOnly, 10);

        if (isNaN(asInt)) {
          return 0;
        } else {
          return asInt;
        }
      }
    } catch(e) {
      // The fallback for any exception is to return 0
    }

    return 0;
  };

  /**
   * IE doesn't have startsWith for String: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/startsWith
   */
  if (!String.prototype.startsWith) {
    String.prototype.startsWith = function (searchString, position) {
      position = position || 0;
      return this.indexOf(searchString, position) === position;
    };
  }

  /**
   * Add endsWith to Strings: http://stackoverflow.com/questions/280634/endswith-in-javascript
   */
  if (!String.prototype.endsWith) {
    String.prototype.endsWith = function(suffix) {
      return this.indexOf(suffix, this.length - suffix.length) !== -1;
    };
  }

  /**
   * IE doesn't have trim: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/Trim
   *
   */
  if(!String.prototype.trim) {
    String.prototype.trim = function () {
      return this.replace(/^\s+|\s+$/g,'');
    };
  }

  return t8;
}));

/*!
 * numeral.js
 * version : 1.5.4
 * author : Adam Draper
 * license : MIT
 * http://adamwdraper.github.com/Numeral-js/
 */

(function () {

    /************************************
        Constants
    ************************************/

    var numeral,
        VERSION = '1.5.3',
        // internal storage for language config files
        languages = {},
        currentLanguage = 'en',
        zeroFormat = null,
        defaultFormat = '0,0',
        defaultCurrencies = ['ADP','AED','AFA','AFN','ALK','ALL','AMD','ANG','AOA','AOK','AON','AOR','ARA','ARL','ARM','ARP','ARS','ATS','AUD','AWG','AZM','AZN','BAD','BAM','BAN','BBD','BDT','BEC','BEF','BEL','BGL','BGM','BGN','BGO','BHD','BIF','BMD','BND','BOB','BOL','BOP','BOV','BRB','BRC','BRE','BRL','BRN','BRR','BRZ','BSD','BTN','BUK','BWP','BYB','BYR','BZD','CAD','CDF','CHE','CHF','CHW','CLE','CLF','CLP','CNX','CNY','COP','COU','CRC','CSD','CSK','CUC','CUP','CVE','CYP','CZK','DDM','DEM','DJF','DKK','DOP','DZD','ECS','ECV','EEK','EGP','ERN','ESA','ESB','ESP','ETB','EUR','FIM','FJD','FKP','FRF','GBP','GEK','GEL','GHC','GHS','GIP','GMD','GNF','GNS','GQE','GRD','GTQ','GWE','GWP','GYD','HKD','HNL','HRD','HRK','HTG','HUF','IDR','IEP','ILP','ILR','ILS','INR','IQD','IRR','ISJ','ISK','ITL','JMD','JOD','JPY','KES','KGS','KHR','KMF','KPW','KRH','KRO','KRW','KWD','KYD','KZT','LAK','LBP','LKR','LRD','LSL','LTL','LTT','LUC','LUF','LUL','LVL','LVR','LYD','MAD','MAF','MCF','MDC','MDL','MGA','MGF','MKD','MKN','MLF','MMK','MNT','MOP','MRO','MTL','MTP','MUR','MVP','MVR','MWK','MXN','MXP','MXV','MYR','MZE','MZM','MZN','NAD','NGN','NIC','NIO','NLG','NOK','NPR','NZD','OMR','PAB','PEI','PEN','PES','PGK','PHP','PKR','PLN','PLZ','PTE','PYG','QAR','RHD','ROL','RON','RSD','RUB','RUR','RWF','SAR','SBD','SCR','SDD','SDG','SDP','SEK','SGD','SHP','SIT','SKK','SLL','SOS','SRD','SRG','SSP','STD','SUR','SVC','SYP','SZL','THB','TJR','TJS','TMM','TMT','TND','TOP','TPE','TRL','TRY','TTD','TWD','TZS','UAH','UAK','UGS','UGX','USD','USN','USS','UYI','UYP','UYU','UZS','VEB','VEF','VND','VNN','VUV','WST','XAF','XAG','XAU','XBA','XBB','XBC','XBD','XCD','XDR','XEU','XFO','XFU','XOF','XPD','XPF','XPT','XRE','XSU','XTS','XUA','XXX','YDD','YER','YUD','YUM','YUN','YUR','ZAL','ZAR','ZMK','ZRN','ZRZ','ZWD','ZWL','ZWR'],
        defaultCurrencyExceptions = {AUD: 'AUD', BRL: 'BRL', CAD: 'CAD', CNY: 'CN\u00A5', DKK: 'DKK', EUR: 'EUR', GBP: 'GBP', HKD: 'HKD', ILS: '\u20AA', INR: 'INR', JPY: 'JPY', KRW: '\u20A9', MXN: 'MX$', NZD: 'NZD', THB: '\u0E3F', TWD: 'NT$', USD: 'US$', VND: '\u20AB', XAF: 'FCFA', XCD: 'EC$', XOF: 'CFA', XPF: 'CFPF'},
        // check for nodeJS
        hasModule = (typeof module !== 'undefined' && module.exports);


    /************************************
        Constructors
    ************************************/


    // Numeral prototype object
    function Numeral (number) {
        this._value = number;
    }

    /**
     * Implementation of toFixed() that treats floats more like decimals
     *
     * Fixes binary rounding issues (eg. (0.615).toFixed(2) === '0.61') that present
     * problems for accounting- and finance-related software.
     */
    function toFixed (value, precision, roundingFunction, optionals) {
        var power = Math.pow(10, precision),
            optionalsRegExp,
            output;
            
        //roundingFunction = (roundingFunction !== undefined ? roundingFunction : Math.round);
        // Multiply up by precision, round accurately, then divide and use native toFixed():
        output = (roundingFunction(value * power) / power).toFixed(precision);

        if (optionals) {
            optionalsRegExp = new RegExp('0{1,' + optionals + '}$');
            output = output.replace(optionalsRegExp, '');
        }

        return output;
    }

    /************************************
        Formatting
    ************************************/

    // determine what type of formatting we need to do
    function formatNumeral (n, format, roundingFunction, config) {
        var output, currencyType, currencyFormatType, lang;

        if(!format){
          lang = languages[currentLanguage];
          if(lang.number && lang.number.defaultFormat) {
            format = lang.number.defaultFormat;
          } else {
            format = defaultFormat;
          }
        }

        // figure out what kind of format we are dealing with
        if (format.indexOf('$') > -1) { // currency!!!!!
            currencyType = (config && config.currency) || null;
            currencyFormatType = (config && config.currencyFormatType) || null;
            if (!currencyFormatType) {
                currencyFormatType = (/a/.test(format)) ? 'abbr' : (/[\d#]+\.[\d#]+/.test(format)) ? 'full' : 'rounded';
            }
            output = formatCurrency(n, currencyType, currencyFormatType, roundingFunction);
        } else if (format.indexOf('%') > -1) { // percentage
            output = formatPercentage(n, format, roundingFunction);
        } else if (format.indexOf(':') > -1) { // time
            output = formatTime(n, format);
        } else { // plain ol' numbers or bytes
            output = formatNumber(n._value, format, roundingFunction);
        }

        // return string
        return output;
    }

    // revert to number
    function unformatNumeral (n, string) {
        var stringOriginal = string,
            thousandRegExp,
            millionRegExp,
            billionRegExp,
            trillionRegExp,
            suffixes = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
            bytesMultiplier = false,
            power;

        if (string.indexOf(':') > -1) {
            n._value = unformatTime(string);
        } else {
            if (string === zeroFormat) {
                n._value = 0;
            } else {
                if (languages[currentLanguage].delimiters.decimal !== '.') {
                    string = string.replace(/\./g,'').replace(languages[currentLanguage].delimiters.decimal, '.');
                }

                // see if abbreviations are there so that we can multiply to the correct number
                thousandRegExp = new RegExp('[^a-zA-Z]' + languages[currentLanguage].abbreviations.thousand + '(?:\\)|( ?\\' + languages[currentLanguage].currency.symbol + ')?(?:\\))?)?$');
                millionRegExp = new RegExp('[^a-zA-Z]' + languages[currentLanguage].abbreviations.million + '(?:\\)|( ?\\' + languages[currentLanguage].currency.symbol + ')?(?:\\))?)?$');
                billionRegExp = new RegExp('[^a-zA-Z]' + languages[currentLanguage].abbreviations.billion + '(?:\\)|( ?\\' + languages[currentLanguage].currency.symbol + ')?(?:\\))?)?$');
                trillionRegExp = new RegExp('[^a-zA-Z]' + languages[currentLanguage].abbreviations.trillion + '(?:\\)|( ?\\' + languages[currentLanguage].currency.symbol + ')?(?:\\))?)?$');

                // see if bytes are there so that we can multiply to the correct number
                for (power = 0; power <= suffixes.length; power++) {
                    bytesMultiplier = (string.indexOf(suffixes[power]) > -1) ? Math.pow(1024, power + 1) : false;

                    if (bytesMultiplier) {
                        break;
                    }
                }

                // do some math to create our number
                n._value = ((bytesMultiplier) ? bytesMultiplier : 1) * ((stringOriginal.match(thousandRegExp)) ? Math.pow(10, 3) : 1) * ((stringOriginal.match(millionRegExp)) ? Math.pow(10, 6) : 1) * ((stringOriginal.match(billionRegExp)) ? Math.pow(10, 9) : 1) * ((stringOriginal.match(trillionRegExp)) ? Math.pow(10, 12) : 1) * ((string.indexOf('%') > -1) ? 0.01 : 1) * (((string.split('-').length + Math.min(string.split('(').length-1, string.split(')').length-1)) % 2)? 1: -1) * Number(string.replace(/[^0-9\.]+/g, ''));

                // round if we are talking about bytes
                n._value = (bytesMultiplier) ? Math.ceil(n._value) : n._value;
            }
        }
        return n._value;
    }

    function formatCurrency (n, currency, formatType, roundingFunction) {
        var lang = languages[currentLanguage],
            currencyExceptions = lang.currency.exceptions || defaultCurrencyExceptions,
            isNegative = n._value < 0,
            formattedNumber, output, currencyFormat, currencySymbol, format, formatTypeLookupKey;

        formatType = formatType || 'full';
        currencyFormat = lang.currency.format[formatType];
        formatTypeLookupKey = isNegative ? 'negative_' + formatType : formatType;

        if (isNegative && lang.currency.format[formatTypeLookupKey]) {
          currencyFormat = lang.currency.format[formatTypeLookupKey];
        }

        //if currency is an exception from default format
        if(lang.currency.format.exceptions && lang.currency.format.exceptions[currency] && lang.currency.format.exceptions[currency][formatTypeLookupKey]) {
          currencyFormat = lang.currency.format.exceptions[currency][formatTypeLookupKey];
        }

        format = currencyFormat.replace(/(?: ?\$ ? ?)|\-/g, '').replace(' ','').replace('\n','');
        if (!currency) {
            currencySymbol = lang.currency.symbol;
        } else if (defaultCurrencies.indexOf(currency) > -1) {
            if (currencyExceptions[currency]) {
                currencySymbol = currencyExceptions[currency];
            } else {
                currencySymbol = currency;
            }
        } else {
            throw new Error(currency + ' is not a valid currency');
        }
        // format the number
        formattedNumber = formatNumber(n._value, format, roundingFunction).replace(/[\(\-\)]/g,'');
        output = currencyFormat.replace(/(?:#[,\. ]##0[.,]00|0[,.]00 ?a|#[,. ]###|#[,\. ]##)/, formattedNumber);
        var dollarIndex = output.indexOf('$');
        if (dollarIndex > -1) {
            output = output.substring(0, dollarIndex) + currencySymbol + output.substring(dollarIndex + 1);
        }

        return output;
    }

    function formatPercentage (n, format, roundingFunction) {
        var space = '',
            output,
            value = n._value * 100;

        // check for space before %
        if (format.indexOf(' %') > -1) {
            space = ' ';
            format = format.replace(' %', '');
        } else {
            format = format.replace('%', '');
        }

        output = formatNumber(value, format, roundingFunction);

        if (output.indexOf(')') > -1 ) {
            output = output.split('');
            output.splice(-1, 0, space + '%');
            output = output.join('');
        } else {
            output = output + space + '%';
        }

        return output;
    }

    function formatTime (n) {
        var hours = Math.floor(n._value/60/60),
            minutes = Math.floor((n._value - (hours * 60 * 60))/60),
            seconds = Math.round(n._value - (hours * 60 * 60) - (minutes * 60));
        return hours + ':' + ((minutes < 10) ? '0' + minutes : minutes) + ':' + ((seconds < 10) ? '0' + seconds : seconds);
    }

    function unformatTime (string) {
        var timeArray = string.split(':'),
            seconds = 0;
        // turn hours and minutes into seconds and add them all up
        if (timeArray.length === 3) {
            // hours
            seconds = seconds + (Number(timeArray[0]) * 60 * 60);
            // minutes
            seconds = seconds + (Number(timeArray[1]) * 60);
            // seconds
            seconds = seconds + Number(timeArray[2]);
        } else if (timeArray.length === 2) {
            // minutes
            seconds = seconds + (Number(timeArray[0]) * 60);
            // seconds
            seconds = seconds + Number(timeArray[1]);
        }
        return Number(seconds);
    }

    function formatNumber (value, format, roundingFunction) {
        var negP = false,
            signed = false,
            optDec = false,
            abbr = '',
            bytes = '',
            ord = '',
            abs = Math.abs(value),
            suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
            min,
            max,
            power,
            w,
            precision,
            thousands,
            d = '',
            neg = false,
            formatted,
            isRtlLang = languages[currentLanguage].isRtl;

        // check if number is zero and a custom zero format has been set
        if (value === 0 && zeroFormat !== null) {
            return zeroFormat;
        } else {
            // see if we should use parentheses for negative number or if we should prefix with a sign
            // if both are present we default to parentheses
            if (format.indexOf('(') > -1) {
                negP = true;
                format = format.slice(1, -1);
            } else if (format.indexOf('+') > -1) {
                signed = true;
                format = format.replace(/\+/g, '');
            }

            // see if abbreviation is wanted
            if (format.indexOf('a') > -1) {
                // check for space before abbreviation
                if (format.indexOf(' a') > -1) {
                    abbr = ' ';
                    format = format.replace(' a', '');
                } else {
                    format = format.replace('a', '');
                }

                if (abs >= Math.pow(10, 12)) {
                    // trillion
                    abbr = abbr + languages[currentLanguage].abbreviations.trillion;
                    value = value / Math.pow(10, 12);
                } else if (abs < Math.pow(10, 12) && abs >= Math.pow(10, 9)) {
                    // billion
                    abbr = abbr + languages[currentLanguage].abbreviations.billion;
                    value = value / Math.pow(10, 9);
                } else if (abs < Math.pow(10, 9) && abs >= Math.pow(10, 6)) {
                    // million
                    abbr = abbr + languages[currentLanguage].abbreviations.million;
                    value = value / Math.pow(10, 6);
                } else if (abs < Math.pow(10, 6) && abs >= Math.pow(10, 3)) {
                    // thousand
                    abbr = abbr + languages[currentLanguage].abbreviations.thousand;
                    value = value / Math.pow(10, 3);
                }
            }

            // see if we are formatting bytes
            if (format.indexOf('b') > -1) {
                // check for space before
                if (format.indexOf(' b') > -1) {
                    bytes = ' ';
                    format = format.replace(' b', '');
                } else {
                    format = format.replace('b', '');
                }

                for (power = 0; power <= suffixes.length; power++) {
                    min = Math.pow(1024, power);
                    max = Math.pow(1024, power+1);

                    if (value >= min && value < max) {
                        bytes = bytes + suffixes[power];
                        if (min > 0) {
                            value = value / min;
                        }
                        break;
                    }
                }
            }

            // see if ordinal is wanted
            if (format.indexOf('o') > -1) {
                // check for space before
                if (format.indexOf(' o') > -1) {
                    ord = ' ';
                    format = format.replace(' o', '');
                } else {
                    format = format.replace('o', '');
                }

                ord = ord + languages[currentLanguage].ordinal(value);
            }

            if (format.indexOf('[.]') > -1) {
                optDec = true;
                format = format.replace('[.]', '.');
            }

            w = value.toString().split('.')[0];


            precision = format.split('.')[1];
            thousands = format.indexOf(',');

            if (precision) {
                if (precision.indexOf('[') > -1) {
                    precision = precision.replace(']', '');
                    precision = precision.split('[');
                    d = toFixed(value, (precision[0].length + precision[1].length), roundingFunction, precision[1].length);
                } else {
                    d = toFixed(value, precision.length, roundingFunction);
                }

                w = d.split('.')[0];

                if (d.split('.')[1].length) {
                    d = languages[currentLanguage].delimiters.decimal + d.split('.')[1];
                } else {
                    d = '';
                }

                if (optDec && Number(d.slice(1)) === 0) {
                    d = '';
                }
            } else {
                w = toFixed(value, null, roundingFunction);
            }

            // format number
            if (w.indexOf('-') > -1) {
                w = w.slice(1);
                neg = true;
            }

            if (thousands > -1) {
                w = w.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1' + languages[currentLanguage].delimiters.thousands);
            }

            if (format.indexOf('.') === 0) {
                w = '';
            }

            formatted = w + d;

            if (ord) {
              formatted += ord;
            }

            if(abbr) {
              formatted += abbr;
            }

            if(bytes) {
              formatted += bytes;
            }

            if (neg) {
              if (negP) {
                formatted = '(' + formatted + ')';
              } else {
                if(isRtlLang) {
                  formatted += '-';
                } else {
                  formatted = '-' + formatted ;
                }
              }
            } else if (signed) {
              if(isRtlLang) {
                formatted += '+';
              } else {
                formatted = '+' + formatted;
              }
            }

          return  formatted;
        }
    }

    /************************************
        Top Level Functions
    ************************************/

    numeral = function (input) {
        if (numeral.isNumeral(input)) {
            input = input.value();
        } else if (input === 0 || typeof input === 'undefined') {
            input = 0;
        } else if (!Number(input)) {
            input = numeral.fn.unformat(input);
        }

        return new Numeral(Number(input));
    };

    // version number
    numeral.version = VERSION;

    // compare numeral object
    numeral.isNumeral = function (obj) {
        return obj instanceof Numeral;
    };

    // This function will load languages and then set the global language.  If
    // no arguments are passed in, it will simply return the current global
    // language key.
    numeral.language = function (key, values) {
        if (!key) {
            return currentLanguage;
        }

        if (key && !values) {
            if(!languages[key]) {
                throw new Error('Unknown language : ' + key);
            }
            currentLanguage = key;
        }

        if (values || !languages[key]) {
            loadLanguage(key, values);
        }

        return numeral;
    };
    
    // This function provides access to the loaded language data.  If
    // no arguments are passed in, it will simply return the current
    // global language object.
    numeral.languageData = function (key) {
        if (!key) {
            return languages[currentLanguage];
        }
        
        if (!languages[key]) {
            throw new Error('Unknown language : ' + key);
        }
        
        return languages[key];
    };

    numeral.language('en', {
        delimiters: {
            thousands: ',',
            decimal: '.'
        },
        abbreviations: {
            thousand: 'k',
            million: 'm',
            billion: 'b',
            trillion: 't'
        },
        ordinal: function (number) {
            var b = number % 10;
            return (~~ (number % 100 / 10) === 1) ? 'th' :
                (b === 1) ? 'st' :
                (b === 2) ? 'nd' :
                (b === 3) ? 'rd' : 'th';
        },
        number : {
          defaultFormat : '#,##0[.]00[0]'
        },
        currency: {
            localCurrency: 'USD',
            format: {
                full: '$ #,##0.00',
                negative_full: '($ #,##0.00)',
                abbr: '$ 0.00a',
                negative_abbr: '($ 0.00a)',
                rounded: '$ #,###',
                negative_rounded: '($ #,###)',
                exceptions : {
                  INR : {
                    full: '$ #,###',
                    negative_full: '($ #,##)'
                  },
                  JPY : {
                    full: '$ #,###',
                    negative_full: '($ #,##)'
                  },
                  USD : {
                    full: '$#,##0.00',
                    negative_full: '($#,##0.00)'
                  }
                }
            },
            symbol: '$'
        }
    });

    numeral.zeroFormat = function (format) {
        zeroFormat = typeof(format) === 'string' ? format : null;
    };

    numeral.defaultFormat = function (format) {
      defaultFormat = typeof(format) === 'string' ? format : '0.0';
    };

    /************************************
        Helpers
    ************************************/

    function loadLanguage(key, values) {
        languages[key] = values;
    }

    /************************************
        Numeral Prototype
    ************************************/


    numeral.fn = Numeral.prototype = {

        clone : function () {
            return numeral(this);
        },

        format : function (inputString, roundingFunction, currency) {
            return formatNumeral(this,
                  inputString,
                  (roundingFunction !== undefined) ? roundingFunction : Math.round,
                  currency ? {currency: currency} : null
              );
        },

        unformat : function (inputString) {
            if (Object.prototype.toString.call(inputString) === '[object Number]') { 
                return inputString; 
            }
            return unformatNumeral(this, inputString ? inputString : defaultFormat);
        },

        formatCurrency : function (formatType, currency, roundingFunction) {
          return formatCurrency(this,
                  currency,
                  formatType,
                  (roundingFunction !== undefined) ? roundingFunction : Math.round
            );
        },

        value : function () {
            return this._value;
        },

        valueOf : function () {
            return this._value;
        },

        set : function (value) {
            this._value = Number(value);
            return this;
        },

        add : function (value) {
            this._value = this._value + Number(value);
            return this;
        },

        subtract : function (value) {
            this._value = this._value - Number(value);
            return this;
        },

        multiply : function (value) {
            this._value = this._value * Number(value);
            return this;
        },

        divide : function (value) {
            this._value = this._value / Number(value);
            return this;
        },

        difference : function (value) {
            var difference = this._value - Number(value);

            if (difference < 0) {
                difference = -difference;
            }

            return difference;
        }

    };

    /************************************
        Exposing Numeral
    ************************************/

    // CommonJS module is defined
    if (hasModule) {
        module.exports = numeral;
    }

    /*global ender:false */
    if (typeof ender === 'undefined') {
        // here, `this` means `window` in the browser, or `global` on the server
        // add `numeral` as a global object via a string identifier,
        // for Closure Compiler 'advanced' mode
        this['numeral'] = numeral;
    }
}).call(this);

//! moment.js
//! version : 2.4.3
//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
//! license : MIT
//! momentjs.com

(function (undefined) {

    /************************************
        Constants
    ************************************/

    var moment,
        VERSION = "2.4.3",
        global = this,
        round = Math.round,
        i,

        YEAR = 0,
        MONTH = 1,
        DATE = 2,
        HOUR = 3,
        MINUTE = 4,
        SECOND = 5,
        MILLISECOND = 6,

        // internal storage for language config files
        languages = {},

        // check for nodeJS
        hasModule = (typeof module !== 'undefined' && module.exports && typeof require !== 'undefined'),

        // ASP.NET json date format regex
        aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
        aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,

        // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
        // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
        isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,

        // format tokens
        formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,
        localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
        localFormattingTokensMD = /(\[[^\[]*\])|(\\)?(UT|UU?U?U?|u{1,4})/g,
        localFormattingTokensMY = /(\[[^\[]*\])|(\\)?(RT|RR?R?R?|r{1,4})/g,

        // parsing token regexes
        parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
        parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
        parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999
        parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
        parseTokenDigits = /\d+/, // nonzero number of digits
        parseTokenWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i, // any word (or two) characters or numbers including two/three word month in arabic.
        parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/i, // +00:00 -00:00 +0000 -0000 or Z
        parseTokenT = /T/i, // T (ISO separator)
        parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123

        //strict parsing regexes
        parseTokenOneDigit = /\d/, // 0 - 9
        parseTokenTwoDigits = /\d\d/, // 00 - 99
        parseTokenThreeDigits = /\d{3}/, // 000 - 999
        parseTokenFourDigits = /\d{4}/, // 0000 - 9999
        parseTokenSixDigits = /[+\-]?\d{6}/, // -999,999 - 999,999

        // preliminary iso regex
        // 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000)
        isoRegex = /^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d:?\d\d|\s*Z)?)?$/,

        isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',

        isoDates = [
            'YYYY-MM-DD',
            'GGGG-[W]WW',
            'GGGG-[W]WW-E',
            'YYYY-DDD'
        ],

        // iso time formats and regexes
        isoTimes = [
            ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/],
            ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
            ['HH:mm', /(T| )\d\d:\d\d/],
            ['HH', /(T| )\d\d/]
        ],

        // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
        parseTimezoneChunker = /([\+\-]|\d\d)/gi,

        // getter and setter names
        proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
        unitMillisecondFactors = {
            'Milliseconds' : 1,
            'Seconds' : 1e3,
            'Minutes' : 6e4,
            'Hours' : 36e5,
            'Days' : 864e5,
            'Months' : 2592e6,
            'Years' : 31536e6
        },

        unitAliases = {
            ms : 'millisecond',
            s : 'second',
            m : 'minute',
            h : 'hour',
            d : 'day',
            D : 'date',
            w : 'week',
            W : 'isoWeek',
            M : 'month',
            y : 'year',
            DDD : 'dayOfYear',
            e : 'weekday',
            E : 'isoWeekday',
            gg: 'weekYear',
            GG: 'isoWeekYear'
        },

        camelFunctions = {
            dayofyear : 'dayOfYear',
            isoweekday : 'isoWeekday',
            isoweek : 'isoWeek',
            weekyear : 'weekYear',
            isoweekyear : 'isoWeekYear'
        },

        // format function strings
        formatFunctions = {},

        // tokens to ordinalize and pad
        ordinalizeTokens = 'DDD w W M D d'.split(' '),
        paddedTokens = 'M D H h m s w W'.split(' '),

        formatTokenFunctions = {
            M    : function () {
                return this.month() + 1;
            },
            MMM  : function (format) {
                return this.lang().monthsShort(this, format);
            },
            MMMM : function (format) {
                return this.lang().months(this, format);
            },
            D    : function () {
                return this.date();
            },
            DDD  : function () {
                return this.dayOfYear();
            },
            d    : function () {
                return this.day();
            },
            dd   : function (format) {
                return this.lang().weekdaysMin(this, format);
            },
            ddd  : function (format) {
                return this.lang().weekdaysShort(this, format);
            },
            dddd : function (format) {
                return this.lang().weekdays(this, format);
            },
            w    : function () {
                return this.week();
            },
            W    : function () {
                return this.isoWeek();
            },
            YY   : function () {
                return leftZeroFill(this.year() % 100, 2);
            },
            YYYY : function () {
                return leftZeroFill(this.year(), 4);
            },
            YYYYY : function () {
                return leftZeroFill(this.year(), 5);
            },
            gg   : function () {
                return leftZeroFill(this.weekYear() % 100, 2);
            },
            gggg : function () {
                return this.weekYear();
            },
            ggggg : function () {
                return leftZeroFill(this.weekYear(), 5);
            },
            GG   : function () {
                return leftZeroFill(this.isoWeekYear() % 100, 2);
            },
            GGGG : function () {
                return this.isoWeekYear();
            },
            GGGGG : function () {
                return leftZeroFill(this.isoWeekYear(), 5);
            },
            e : function () {
                return this.weekday();
            },
            E : function () {
                return this.isoWeekday();
            },
            a    : function () {
                return this.lang().meridiem(this.hours(), this.minutes(), true);
            },
            A    : function () {
                return this.lang().meridiem(this.hours(), this.minutes(), false);
            },
            H    : function () {
                return this.hours();
            },
            h    : function () {
                return this.hours() % 12 || 12;
            },
            m    : function () {
                return this.minutes();
            },
            s    : function () {
                return this.seconds();
            },
            S    : function () {
                return toInt(this.milliseconds() / 100);
            },
            SS   : function () {
                return leftZeroFill(toInt(this.milliseconds() / 10), 2);
            },
            SSS  : function () {
                return leftZeroFill(this.milliseconds(), 3);
            },
            SSSS : function () {
                return leftZeroFill(this.milliseconds(), 3);
            },
            Z    : function () {
                var a = -this.zone(),
                    b = "+";
                if (a < 0) {
                    a = -a;
                    b = "-";
                }
                return b + leftZeroFill(toInt(a / 60), 2) + ":" + leftZeroFill(toInt(a) % 60, 2);
            },
            ZZ   : function () {
                var a = -this.zone(),
                    b = "+";
                if (a < 0) {
                    a = -a;
                    b = "-";
                }
                return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2);
            },
            z : function () {
                return this.zoneAbbr();
            },
            zz : function () {
                return this.zoneName();
            },
            X    : function () {
                return this.unix();
            },
            Q : function () {
                return this.quarter();
            }
        },

        lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'];

    function padToken(func, count) {
        return function (a) {
            return leftZeroFill(func.call(this, a), count);
        };
    }
    function ordinalizeToken(func, period) {
        return function (a) {
            return this.lang().ordinal(func.call(this, a), period);
        };
    }

    while (ordinalizeTokens.length) {
        i = ordinalizeTokens.pop();
        formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i);
    }
    while (paddedTokens.length) {
        i = paddedTokens.pop();
        formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
    }
    formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);


    /************************************
        Constructors
    ************************************/

    function Language() {

    }

    // Moment prototype object
    function Moment(config) {
        checkOverflow(config);
        extend(this, config);
    }

    // Duration Constructor
    function Duration(duration) {
        var normalizedInput = normalizeObjectUnits(duration),
            years = normalizedInput.year || 0,
            months = normalizedInput.month || 0,
            weeks = normalizedInput.week || 0,
            days = normalizedInput.day || 0,
            hours = normalizedInput.hour || 0,
            minutes = normalizedInput.minute || 0,
            seconds = normalizedInput.second || 0,
            milliseconds = normalizedInput.millisecond || 0;

        // representation for dateAddRemove
        this._milliseconds = +milliseconds +
            seconds * 1e3 + // 1000
            minutes * 6e4 + // 1000 * 60
            hours * 36e5; // 1000 * 60 * 60
        // Because of dateAddRemove treats 24 hours as different from a
        // day when working around DST, we need to store them separately
        this._days = +days +
            weeks * 7;
        // It is impossible translate months into days without knowing
        // which months you are are talking about, so we have to store
        // it separately.
        this._months = +months +
            years * 12;

        this._data = {};

        this._bubble();
    }

    /************************************
        Helpers
    ************************************/


    function extend(a, b) {
        for (var i in b) {
            if (b.hasOwnProperty(i)) {
                a[i] = b[i];
            }
        }

        if (b.hasOwnProperty("toString")) {
            a.toString = b.toString;
        }

        if (b.hasOwnProperty("valueOf")) {
            a.valueOf = b.valueOf;
        }

        return a;
    }

    function absRound(number) {
        if (number < 0) {
            return Math.ceil(number);
        } else {
            return Math.floor(number);
        }
    }

    // left zero fill a number
    // see http://jsperf.com/left-zero-filling for performance comparison
    function leftZeroFill(number, targetLength) {
        var output = number + '';
        while (output.length < targetLength) {
            output = '0' + output;
        }
        return output;
    }

    // helper function for _.addTime and _.subtractTime
    function addOrSubtractDurationFromMoment(mom, duration, isAdding, ignoreUpdateOffset) {
        var milliseconds = duration._milliseconds,
            days = duration._days,
            months = duration._months,
            minutes,
            hours;

        if (milliseconds) {
            mom._d.setTime(+mom._d + milliseconds * isAdding);
        }
        // store the minutes and hours so we can restore them
        if (days || months) {
            minutes = mom.minute();
            hours = mom.hour();
        }
        if (days) {
            mom.date(mom.date() + days * isAdding);
        }
        if (months) {
            mom.month(mom.month() + months * isAdding);
        }
        if (milliseconds && !ignoreUpdateOffset) {
            moment.updateOffset(mom);
        }
        // restore the minutes and hours after possibly changing dst
        if (days || months) {
            mom.minute(minutes);
            mom.hour(hours);
        }
    }

    // check if is an array
    function isArray(input) {
        return Object.prototype.toString.call(input) === '[object Array]';
    }

    function isDate(input) {
        return  Object.prototype.toString.call(input) === '[object Date]' ||
                input instanceof Date;
    }

    // compare two arrays, return the number of differences
    function compareArrays(array1, array2, dontConvert) {
        var len = Math.min(array1.length, array2.length),
            lengthDiff = Math.abs(array1.length - array2.length),
            diffs = 0,
            i;
        for (i = 0; i < len; i++) {
            if ((dontConvert && array1[i] !== array2[i]) ||
                (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
                diffs++;
            }
        }
        return diffs + lengthDiff;
    }

    function normalizeUnits(units) {
        if (units) {
            var lowered = units.toLowerCase().replace(/(.)s$/, '$1');
            units = unitAliases[units] || camelFunctions[lowered] || lowered;
        }
        return units;
    }

    function normalizeObjectUnits(inputObject) {
        var normalizedInput = {},
            normalizedProp,
            prop;

        for (prop in inputObject) {
            if (inputObject.hasOwnProperty(prop)) {
                normalizedProp = normalizeUnits(prop);
                if (normalizedProp) {
                    normalizedInput[normalizedProp] = inputObject[prop];
                }
            }
        }

        return normalizedInput;
    }

    function makeList(field) {
        var count, setter;

        if (field.indexOf('week') === 0) {
            count = 7;
            setter = 'day';
        }
        else if (field.indexOf('month') === 0) {
            count = 12;
            setter = 'month';
        }
        else {
            return;
        }

        moment[field] = function (format, index) {
            var i, getter,
                method = moment.fn._lang[field],
                results = [];

            if (typeof format === 'number') {
                index = format;
                format = undefined;
            }

            getter = function (i) {
                var m = moment().utc().set(setter, i);
                return method.call(moment.fn._lang, m, format || '');
            };

            if (index != null) {
                return getter(index);
            }
            else {
                for (i = 0; i < count; i++) {
                    results.push(getter(i));
                }
                return results;
            }
        };
    }

    function toInt(argumentForCoercion) {
        var coercedNumber = +argumentForCoercion,
            value = 0;

        if (coercedNumber !== 0 && isFinite(coercedNumber)) {
            if (coercedNumber >= 0) {
                value = Math.floor(coercedNumber);
            } else {
                value = Math.ceil(coercedNumber);
            }
        }

        return value;
    }

    function daysInMonth(year, month) {
        return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
    }

    function daysInYear(year) {
        return isLeapYear(year) ? 366 : 365;
    }

    function isLeapYear(year) {
        return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
    }

    function checkOverflow(m) {
        var overflow;
        if (m._a && m._pf.overflow === -2) {
            overflow =
                m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH :
                m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE :
                m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR :
                m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE :
                m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND :
                m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND :
                -1;

            if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
                overflow = DATE;
            }

            m._pf.overflow = overflow;
        }
    }

    function initializeParsingFlags(config) {
        config._pf = {
            empty : false,
            unusedTokens : [],
            unusedInput : [],
            overflow : -2,
            charsLeftOver : 0,
            nullInput : false,
            invalidMonth : null,
            invalidFormat : false,
            userInvalidated : false,
            iso: false
        };
    }

    function isValid(m) {
        if (m._isValid == null) {
            m._isValid = !isNaN(m._d.getTime()) &&
                m._pf.overflow < 0 &&
                !m._pf.empty &&
                !m._pf.invalidMonth &&
                !m._pf.nullInput &&
                !m._pf.invalidFormat &&
                !m._pf.userInvalidated;

            if (m._strict) {
                m._isValid = m._isValid &&
                    m._pf.charsLeftOver === 0 &&
                    m._pf.unusedTokens.length === 0;
            }
        }
        return m._isValid;
    }

    function normalizeLanguage(key) {
        return key ? key.toLowerCase().replace('_', '-') : key;
    }

    /************************************
        Languages
    ************************************/


    extend(Language.prototype, {

        set : function (config) {
            var prop, i;
            for (i in config) {
                prop = config[i];
                if (typeof prop === 'function') {
                    this[i] = prop;
                } else {
                    this['_' + i] = prop;
                }
            }
        },

        _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
        months : function (m) {
            return this._months[m.month()];
        },

        _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
        monthsShort : function (m) {
            return this._monthsShort[m.month()];
        },

        monthsParse : function (monthName) {
            var i, mom, regex;

            if (!this._monthsParse) {
                this._monthsParse = [];
            }

            for (i = 0; i < 12; i++) {
                // make the regex if we don't have it already
                if (!this._monthsParse[i]) {
                    mom = moment.utc([2000, i]);
                    regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
                    this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
                }
                // test the regex
                if (this._monthsParse[i].test(monthName)) {
                    return i;
                }
            }
        },

        _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
        weekdays : function (m) {
            return this._weekdays[m.day()];
        },

        _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
        weekdaysShort : function (m) {
            return this._weekdaysShort[m.day()];
        },

        _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
        weekdaysMin : function (m) {
            return this._weekdaysMin[m.day()];
        },

        weekdaysParse : function (weekdayName) {
            var i, mom, regex;

            if (!this._weekdaysParse) {
                this._weekdaysParse = [];
            }

            for (i = 0; i < 7; i++) {
                // make the regex if we don't have it already
                if (!this._weekdaysParse[i]) {
                    mom = moment([2000, 1]).day(i);
                    regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
                    this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
                }
                // test the regex
                if (this._weekdaysParse[i].test(weekdayName)) {
                    return i;
                }
            }
        },

        _longDateFormat : {
            LT : "h:mm A",
            L : "MM/DD/YYYY",
            l : "M/DD/YYYY",
            LL : "MMMM D, YYYY",
            LLL : "MMMM D YYYY LT",
            LLLL : "dddd, MMMM D YYYY LT"
        },
        _longDateFormat_MD : {
            UT : "h:mm A",
            U : "MM/DD",
            u : "MMM D",
            UU : "MMMM D",
            UUU : "MMMM D",
            UUUU : "dddd, MMMM D UT"
        },
        _longDateFormat_MY : {
            RT : "h:mm A",
            R : "MM/YYYY",
            r : "MMM YYYY",
            RR : "MMMM YYYY",
            RRR : "MMMM YYYY",
            RRRR : "dddd, MMMM YYYY RT"
        },
        longDateFormat : function (key) {
            var output = this._longDateFormat[key];
            if (!output && this._longDateFormat[key.toUpperCase()]) {
                output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
                    return val.slice(1);
                });
                this._longDateFormat[key] = output;
            }
            return output;
        },
        longDateFormatMD : function (key) {
          var formatLookup = (typeof this._longDateFormatMD !== 'undefined')? this._longDateFormatMD : this._longDateFormat_MD,
              output= formatLookup[key];
          if (!output && formatLookup[key.toUpperCase()]) {
            output = formatLookup[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
              return val.slice(1);
            });
            formatLookup[key] = output;
          }
          return output;
        },
        longDateFormatMY : function (key) {
          var formatLookup = (typeof this._longDateFormatMD !== 'undefined')? this._longDateFormatMY : this._longDateFormat_MY,
              output = formatLookup[key];
          if (!output && formatLookup[key.toUpperCase()]) {
            output = formatLookup[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
              return val.slice(1);
            });
            formatLookup[key] = output;
          }
          return output;
        },

        isPM : function (input) {
            // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
            // Using charAt should be more compatible.
            return ((input + '').toLowerCase().charAt(0) === 'p');
        },

        _meridiemParse : /[ap]\.?m?\.?/i,
        meridiem : function (hours, minutes, isLower) {
            if (hours > 11) {
                return isLower ? 'pm' : 'PM';
            } else {
                return isLower ? 'am' : 'AM';
            }
        },

        _calendar : {
            sameDay : '[Today at] LT',
            nextDay : '[Tomorrow at] LT',
            nextWeek : 'dddd [at] LT',
            lastDay : '[Yesterday at] LT',
            lastWeek : '[Last] dddd [at] LT',
            sameElse : 'L'
        },
        calendar : function (key, mom) {
            var output = this._calendar[key];
            return typeof output === 'function' ? output.apply(mom) : output;
        },

        _relativeTime : {
            future : "in %s",
            past : "%s ago",
            s : "a few seconds",
            m : "a minute",
            mm : "%d minutes",
            h : "an hour",
            hh : "%d hours",
            d : "a day",
            dd : "%d days",
            M : "a month",
            MM : "%d months",
            y : "a year",
            yy : "%d years"
        },
        relativeTime : function (number, withoutSuffix, string, isFuture) {
            var output = this._relativeTime[string];
            return (typeof output === 'function') ?
                output(number, withoutSuffix, string, isFuture) :
                output.replace(/%d/i, number);
        },
        pastFuture : function (diff, output) {
            var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
            return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
        },

        ordinal : function (number) {
            return this._ordinal.replace("%d", number);
        },
        _ordinal : "%d",

        preparse : function (string) {
            return string;
        },

        postformat : function (string) {
            return string;
        },

        week : function (mom) {
            return weekOfYear(mom, this._week.dow, this._week.doy).week;
        },

        _week : {
            dow : 0, // Sunday is the first day of the week.
            doy : 6  // The week that contains Jan 1st is the first week of the year.
        },

        _invalidDate: 'Invalid date',
        invalidDate: function () {
            return this._invalidDate;
        }
    });

    // Loads a language definition into the `languages` cache.  The function
    // takes a key and optionally values.  If not in the browser and no values
    // are provided, it will load the language file module.  As a convenience,
    // this function also returns the language values.
    function loadLang(key, values) {
        values.abbr = key;
        if (!languages[key]) {
            languages[key] = new Language();
        }
        languages[key].set(values);
        return languages[key];
    }

    // Remove a language from the `languages` cache. Mostly useful in tests.
    function unloadLang(key) {
        delete languages[key];
    }

    // Determines which language definition to use and returns it.
    //
    // With no parameters, it will return the global language.  If you
    // pass in a language key, such as 'en', it will return the
    // definition for 'en', so long as 'en' has already been loaded using
    // moment.lang.
    function getLangDefinition(key) {
        var i = 0, j, lang, next, split,
            get = function (k) {
                if (!languages[k] && hasModule) {
                    try {
                        require('./lang/' + k);
                    } catch (e) { }
                }
                return languages[k];
            };

        if (!key) {
            return moment.fn._lang;
        }

        if (!isArray(key)) {
            //short-circuit everything else
            lang = get(key);
            if (lang) {
                return lang;
            }
            key = [key];
        }

        //pick the language from the array
        //try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
        //substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
        while (i < key.length) {
            split = normalizeLanguage(key[i]).split('-');
            j = split.length;
            next = normalizeLanguage(key[i + 1]);
            next = next ? next.split('-') : null;
            while (j > 0) {
                lang = get(split.slice(0, j).join('-'));
                if (lang) {
                    return lang;
                }
                if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
                    //the next array item is better than a shallower substring of this one
                    break;
                }
                j--;
            }
            i++;
        }
        return moment.fn._lang;
    }

    /************************************
        Formatting
    ************************************/


    function removeFormattingTokens(input) {
        if (input.match(/\[[\s\S]/)) {
            return input.replace(/^\[|\]$/g, "");
        }
        return input.replace(/\\/g, "");
    }

    function makeFormatFunction(format) {
        var array = format.match(formattingTokens), i, length;

        for (i = 0, length = array.length; i < length; i++) {
            if (formatTokenFunctions[array[i]]) {
                array[i] = formatTokenFunctions[array[i]];
            } else {
                array[i] = removeFormattingTokens(array[i]);
            }
        }

        return function (mom) {
            var output = "";
            for (i = 0; i < length; i++) {
                output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
            }
            return output;
        };
    }

    // format date using native date object
    function formatMoment(m, format) {

        if (!m.isValid()) {
            return m.lang().invalidDate();
        }

        format = expandFormat(format, m.lang());

        if (!formatFunctions[format]) {
            formatFunctions[format] = makeFormatFunction(format);
        }

        return formatFunctions[format](m);
    }

    function expandFormat(format, lang) {
        var i = 5, j = 5, k = 5;

        function replaceLongDateFormatTokens(input) {
            return lang.longDateFormat(input) || input;
        }
        function replaceLongDateFormatTokensMD(input) {
            return lang.longDateFormatMD(input) || input;
        }
        function replaceLongDateFormatTokensMY(input) {
            return lang.longDateFormatMY(input) || input;
        }

        localFormattingTokens.lastIndex = 0;
        localFormattingTokensMD.lastIndex = 0;
        localFormattingTokensMY.lastIndex = 0;
        
        while (i >= 0 && localFormattingTokens.test(format)) {
            format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
            localFormattingTokens.lastIndex = 0;
            i -= 1;
        }
        while (j >= 0 && localFormattingTokensMD.test(format)) {
            format = format.replace(localFormattingTokensMD, replaceLongDateFormatTokensMD);
            localFormattingTokensMD.lastIndex = 0;
            j -= 1;
        }
        while (k >= 0 && localFormattingTokensMY.test(format)) {
            format = format.replace(localFormattingTokensMY, replaceLongDateFormatTokensMY);
            localFormattingTokensMY.lastIndex = 0;
            k -= 1;
        }

        return format;
    }


    /************************************
        Parsing
    ************************************/


    // get the regex to find the next token
    function getParseRegexForToken(token, config) {
        var a, strict = config._strict;
        switch (token) {
        case 'DDDD':
            return parseTokenThreeDigits;
        case 'YYYY':
        case 'GGGG':
        case 'gggg':
            return strict ? parseTokenFourDigits : parseTokenOneToFourDigits;
        case 'YYYYY':
        case 'GGGGG':
        case 'ggggg':
            return strict ? parseTokenSixDigits : parseTokenOneToSixDigits;
        case 'S':
            if (strict) { return parseTokenOneDigit; }
            /* falls through */
        case 'SS':
            if (strict) { return parseTokenTwoDigits; }
            /* falls through */
        case 'SSS':
        case 'DDD':
            return strict ? parseTokenThreeDigits : parseTokenOneToThreeDigits;
        case 'MMM':
        case 'MMMM':
        case 'dd':
        case 'ddd':
        case 'dddd':
            return parseTokenWord;
        case 'a':
        case 'A':
            return getLangDefinition(config._l)._meridiemParse;
        case 'X':
            return parseTokenTimestampMs;
        case 'Z':
        case 'ZZ':
            return parseTokenTimezone;
        case 'T':
            return parseTokenT;
        case 'SSSS':
            return parseTokenDigits;
        case 'MM':
        case 'DD':
        case 'YY':
        case 'GG':
        case 'gg':
        case 'HH':
        case 'hh':
        case 'mm':
        case 'ss':
        case 'ww':
        case 'WW':
            return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits;
        case 'M':
        case 'D':
        case 'd':
        case 'H':
        case 'h':
        case 'm':
        case 's':
        case 'w':
        case 'W':
        case 'e':
        case 'E':
            return strict ? parseTokenOneDigit : parseTokenOneOrTwoDigits;
        default :
            a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i"));
            return a;
        }
    }

    function timezoneMinutesFromString(string) {
        var tzchunk = (parseTokenTimezone.exec(string) || [])[0],
            parts = (tzchunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
            minutes = +(parts[1] * 60) + toInt(parts[2]);

        return parts[0] === '+' ? -minutes : minutes;
    }

    // function to convert string input to date
    function addTimeToArrayFromToken(token, input, config) {
        var a, datePartArray = config._a;

        switch (token) {
        // MONTH
        case 'M' : // fall through to MM
        case 'MM' :
            if (input != null) {
                datePartArray[MONTH] = toInt(input) - 1;
            }
            break;
        case 'MMM' : // fall through to MMMM
        case 'MMMM' :
            a = getLangDefinition(config._l).monthsParse(input);
            // if we didn't find a month name, mark the date as invalid.
            if (a != null) {
                datePartArray[MONTH] = a;
            } else {
                config._pf.invalidMonth = input;
            }
            break;
        // DAY OF MONTH
        case 'D' : // fall through to DD
        case 'DD' :
            if (input != null) {
                datePartArray[DATE] = toInt(input);
            }
            break;
        // DAY OF YEAR
        case 'DDD' : // fall through to DDDD
        case 'DDDD' :
            if (input != null) {
                config._dayOfYear = toInt(input);
            }

            break;
        // YEAR
        case 'YY' :
            datePartArray[YEAR] = toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
            break;
        case 'YYYY' :
        case 'YYYYY' :
            datePartArray[YEAR] = toInt(input);
            break;
        // AM / PM
        case 'a' : // fall through to A
        case 'A' :
            config._isPm = getLangDefinition(config._l).isPM(input);
            break;
        // 24 HOUR
        case 'H' : // fall through to hh
        case 'HH' : // fall through to hh
        case 'h' : // fall through to hh
        case 'hh' :
            datePartArray[HOUR] = toInt(input);
            break;
        // MINUTE
        case 'm' : // fall through to mm
        case 'mm' :
            datePartArray[MINUTE] = toInt(input);
            break;
        // SECOND
        case 's' : // fall through to ss
        case 'ss' :
            datePartArray[SECOND] = toInt(input);
            break;
        // MILLISECOND
        case 'S' :
        case 'SS' :
        case 'SSS' :
        case 'SSSS' :
            datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000);
            break;
        // UNIX TIMESTAMP WITH MS
        case 'X':
            config._d = new Date(parseFloat(input) * 1000);
            break;
        // TIMEZONE
        case 'Z' : // fall through to ZZ
        case 'ZZ' :
            config._useUTC = true;
            config._tzm = timezoneMinutesFromString(input);
            break;
        case 'w':
        case 'ww':
        case 'W':
        case 'WW':
        case 'd':
        case 'dd':
        case 'ddd':
        case 'dddd':
        case 'e':
        case 'E':
            token = token.substr(0, 1);
            /* falls through */
        case 'gg':
        case 'gggg':
        case 'GG':
        case 'GGGG':
        case 'GGGGG':
            token = token.substr(0, 2);
            if (input) {
                config._w = config._w || {};
                config._w[token] = input;
            }
            break;
        }
    }

    // convert an array to a date.
    // the array should mirror the parameters below
    // note: all values past the year are optional and will default to the lowest possible value.
    // [year, month, day , hour, minute, second, millisecond]
    function dateFromConfig(config) {
        var i, date, input = [], currentDate,
            yearToUse, fixYear, w, temp, lang, weekday, week;

        if (config._d) {
            return;
        }

        currentDate = currentDateArray(config);

        //compute day of the year from weeks and weekdays
        if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
            fixYear = function (val) {
                return val ?
                  (val.length < 3 ? (parseInt(val, 10) > 68 ? '19' + val : '20' + val) : val) :
                  (config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]);
            };

            w = config._w;
            if (w.GG != null || w.W != null || w.E != null) {
                temp = dayOfYearFromWeeks(fixYear(w.GG), w.W || 1, w.E, 4, 1);
            }
            else {
                lang = getLangDefinition(config._l);
                weekday = w.d != null ?  parseWeekday(w.d, lang) :
                  (w.e != null ?  parseInt(w.e, 10) + lang._week.dow : 0);

                week = parseInt(w.w, 10) || 1;

                //if we're parsing 'd', then the low day numbers may be next week
                if (w.d != null && weekday < lang._week.dow) {
                    week++;
                }

                temp = dayOfYearFromWeeks(fixYear(w.gg), week, weekday, lang._week.doy, lang._week.dow);
            }

            config._a[YEAR] = temp.year;
            config._dayOfYear = temp.dayOfYear;
        }

        //if the day of the year is set, figure out what it is
        if (config._dayOfYear) {
            yearToUse = config._a[YEAR] == null ? currentDate[YEAR] : config._a[YEAR];

            if (config._dayOfYear > daysInYear(yearToUse)) {
                config._pf._overflowDayOfYear = true;
            }

            date = makeUTCDate(yearToUse, 0, config._dayOfYear);
            config._a[MONTH] = date.getUTCMonth();
            config._a[DATE] = date.getUTCDate();
        }

        // Default to current date.
        // * if no year, month, day of month are given, default to today
        // * if day of month is given, default month and year
        // * if month is given, default only year
        // * if year is given, don't default anything
        for (i = 0; i < 3 && config._a[i] == null; ++i) {
            config._a[i] = input[i] = currentDate[i];
        }

        // Zero out whatever was not defaulted, including time
        for (; i < 7; i++) {
            config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
        }

        // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
        input[HOUR] += toInt((config._tzm || 0) / 60);
        input[MINUTE] += toInt((config._tzm || 0) % 60);

        config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input);
    }

    function dateFromObject(config) {
        var normalizedInput;

        if (config._d) {
            return;
        }

        normalizedInput = normalizeObjectUnits(config._i);
        config._a = [
            normalizedInput.year,
            normalizedInput.month,
            normalizedInput.day,
            normalizedInput.hour,
            normalizedInput.minute,
            normalizedInput.second,
            normalizedInput.millisecond
        ];

        dateFromConfig(config);
    }

    function currentDateArray(config) {
        var now = new Date();
        if (config._useUTC) {
            return [
                now.getUTCFullYear(),
                now.getUTCMonth(),
                now.getUTCDate()
            ];
        } else {
            return [now.getFullYear(), now.getMonth(), now.getDate()];
        }
    }

    // date from string and format string
    function makeDateFromStringAndFormat(config) {

        config._a = [];
        config._pf.empty = true;

        // This array is used to make a Date, either with `new Date` or `Date.UTC`
        var lang = getLangDefinition(config._l),
            string = '' + config._i,
            i, parsedInput, tokens, token, skipped,
            stringLength = string.length,
            totalParsedInputLength = 0;

        tokens = expandFormat(config._f, lang).match(formattingTokens) || [];

        for (i = 0; i < tokens.length; i++) {
            token = tokens[i];
            parsedInput = (getParseRegexForToken(token, config).exec(string) || [])[0];
            if (parsedInput) {
                skipped = string.substr(0, string.indexOf(parsedInput));
                if (skipped.length > 0) {
                    config._pf.unusedInput.push(skipped);
                }
                string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
                totalParsedInputLength += parsedInput.length;
            }
            // don't parse if it's not a known token
            if (formatTokenFunctions[token]) {
                if (parsedInput) {
                    config._pf.empty = false;
                }
                else {
                    config._pf.unusedTokens.push(token);
                }
                addTimeToArrayFromToken(token, parsedInput, config);
            }
            else if (config._strict && !parsedInput) {
                config._pf.unusedTokens.push(token);
            }
        }

        // add remaining unparsed input length to the string
        config._pf.charsLeftOver = stringLength - totalParsedInputLength;
        if (string.length > 0) {
            config._pf.unusedInput.push(string);
        }

        // handle am pm
        if (config._isPm && config._a[HOUR] < 12) {
            config._a[HOUR] += 12;
        }
        // if is 12 am, change hours to 0
        if (config._isPm === false && config._a[HOUR] === 12) {
            config._a[HOUR] = 0;
        }

        dateFromConfig(config);
        checkOverflow(config);
    }

    function unescapeFormat(s) {
        return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
            return p1 || p2 || p3 || p4;
        });
    }

    // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
    function regexpEscape(s) {
        return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
    }

    // date from string and array of format strings
    function makeDateFromStringAndArray(config) {
        var tempConfig,
            bestMoment,

            scoreToBeat,
            i,
            currentScore;

        if (config._f.length === 0) {
            config._pf.invalidFormat = true;
            config._d = new Date(NaN);
            return;
        }

        for (i = 0; i < config._f.length; i++) {
            currentScore = 0;
            tempConfig = extend({}, config);
            initializeParsingFlags(tempConfig);
            tempConfig._f = config._f[i];
            makeDateFromStringAndFormat(tempConfig);

            if (!isValid(tempConfig)) {
                continue;
            }

            // if there is any input that was not parsed add a penalty for that format
            currentScore += tempConfig._pf.charsLeftOver;

            //or tokens
            currentScore += tempConfig._pf.unusedTokens.length * 10;

            tempConfig._pf.score = currentScore;

            if (scoreToBeat == null || currentScore < scoreToBeat) {
                scoreToBeat = currentScore;
                bestMoment = tempConfig;
            }
        }

        extend(config, bestMoment || tempConfig);
    }

    // date from iso format
    function makeDateFromString(config) {
        var i,
            string = config._i,
            match = isoRegex.exec(string);

        if (match) {
            config._pf.iso = true;
            for (i = 4; i > 0; i--) {
                if (match[i]) {
                    // match[5] should be "T" or undefined
                    config._f = isoDates[i - 1] + (match[6] || " ");
                    break;
                }
            }
            for (i = 0; i < 4; i++) {
                if (isoTimes[i][1].exec(string)) {
                    config._f += isoTimes[i][0];
                    break;
                }
            }
            if (parseTokenTimezone.exec(string)) {
                config._f += "Z";
            }
            makeDateFromStringAndFormat(config);
        }
        else {
            config._d = new Date(string);
        }
    }

    function makeDateFromInput(config) {
        var input = config._i,
            matched = aspNetJsonRegex.exec(input);

        if (input === undefined) {
            config._d = new Date();
        } else if (matched) {
            config._d = new Date(+matched[1]);
        } else if (typeof input === 'string') {
            makeDateFromString(config);
        } else if (isArray(input)) {
            config._a = input.slice(0);
            dateFromConfig(config);
        } else if (isDate(input)) {
            config._d = new Date(+input);
        } else if (typeof(input) === 'object') {
            dateFromObject(config);
        } else {
            config._d = new Date(input);
        }
    }

    function makeDate(y, m, d, h, M, s, ms) {
        //can't just apply() to create a date:
        //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
        var date = new Date(y, m, d, h, M, s, ms);

        //the date constructor doesn't accept years < 1970
        if (y < 1970) {
            date.setFullYear(y);
        }
        return date;
    }

    function makeUTCDate(y) {
        var date = new Date(Date.UTC.apply(null, arguments));
        if (y < 1970) {
            date.setUTCFullYear(y);
        }
        return date;
    }

    function parseWeekday(input, language) {
        if (typeof input === 'string') {
            if (!isNaN(input)) {
                input = parseInt(input, 10);
            }
            else {
                input = language.weekdaysParse(input);
                if (typeof input !== 'number') {
                    return null;
                }
            }
        }
        return input;
    }

    /************************************
        Relative Time
    ************************************/


    // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
    function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
        return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
    }

    function relativeTime(milliseconds, withoutSuffix, lang) {
        var seconds = round(Math.abs(milliseconds) / 1000),
            minutes = round(seconds / 60),
            hours = round(minutes / 60),
            days = round(hours / 24),
            years = round(days / 365),
            args = seconds < 45 && ['s', seconds] ||
                minutes === 1 && ['m'] ||
                minutes < 45 && ['mm', minutes] ||
                hours === 1 && ['h'] ||
                hours < 22 && ['hh', hours] ||
                days === 1 && ['d'] ||
                days <= 25 && ['dd', days] ||
                days <= 45 && ['M'] ||
                days < 345 && ['MM', round(days / 30)] ||
                years === 1 && ['y'] || ['yy', years];
        args[2] = withoutSuffix;
        args[3] = milliseconds > 0;
        args[4] = lang;
        return substituteTimeAgo.apply({}, args);
    }


    /************************************
        Week of Year
    ************************************/


    // firstDayOfWeek       0 = sun, 6 = sat
    //                      the day of the week that starts the week
    //                      (usually sunday or monday)
    // firstDayOfWeekOfYear 0 = sun, 6 = sat
    //                      the first week is the week that contains the first
    //                      of this day of the week
    //                      (eg. ISO weeks use thursday (4))
    function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
        var end = firstDayOfWeekOfYear - firstDayOfWeek,
            daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
            adjustedMoment;


        if (daysToDayOfWeek > end) {
            daysToDayOfWeek -= 7;
        }

        if (daysToDayOfWeek < end - 7) {
            daysToDayOfWeek += 7;
        }

        adjustedMoment = moment(mom).add('d', daysToDayOfWeek);
        return {
            week: Math.ceil(adjustedMoment.dayOfYear() / 7),
            year: adjustedMoment.year()
        };
    }

    //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
    function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) {
        var d = new Date(Date.UTC(year, 0)).getUTCDay(),
            daysToAdd, dayOfYear;

        weekday = weekday != null ? weekday : firstDayOfWeek;
        daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0);
        dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1;

        return {
            year: dayOfYear > 0 ? year : year - 1,
            dayOfYear: dayOfYear > 0 ?  dayOfYear : daysInYear(year - 1) + dayOfYear
        };
    }

    /************************************
        Top Level Functions
    ************************************/

    function makeMoment(config) {
        var input = config._i,
            format = config._f;

        if (typeof config._pf === 'undefined') {
            initializeParsingFlags(config);
        }

        if (input === null) {
            return moment.invalid({nullInput: true});
        }

        if (typeof input === 'string') {
            config._i = input = getLangDefinition().preparse(input);
        }

        if (moment.isMoment(input)) {
            config = extend({}, input);

            config._d = new Date(+input._d);
        } else if (format) {
            if (isArray(format)) {
                makeDateFromStringAndArray(config);
            } else {
                makeDateFromStringAndFormat(config);
            }
        } else {
            makeDateFromInput(config);
        }

        return new Moment(config);
    }

    moment = function (input, format, lang, strict) {
        if (typeof(lang) === "boolean") {
            strict = lang;
            lang = undefined;
        }
        return makeMoment({
            _i : input,
            _f : format,
            _l : lang,
            _strict : strict,
            _isUTC : false
        });
    };

    // creating with utc
    moment.utc = function (input, format, lang, strict) {
        var m;

        if (typeof(lang) === "boolean") {
            strict = lang;
            lang = undefined;
        }
        m = makeMoment({
            _useUTC : true,
            _isUTC : true,
            _l : lang,
            _i : input,
            _f : format,
            _strict : strict
        }).utc();

        return m;
    };

    // creating with unix timestamp (in seconds)
    moment.unix = function (input) {
        return moment(input * 1000);
    };

    // duration
    moment.duration = function (input, key) {
        var duration = input,
            // matching against regexp is expensive, do it on demand
            match = null,
            sign,
            ret,
            parseIso;

        if (moment.isDuration(input)) {
            duration = {
                ms: input._milliseconds,
                d: input._days,
                M: input._months
            };
        } else if (typeof input === 'number') {
            duration = {};
            if (key) {
                duration[key] = input;
            } else {
                duration.milliseconds = input;
            }
        } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) {
            sign = (match[1] === "-") ? -1 : 1;
            duration = {
                y: 0,
                d: toInt(match[DATE]) * sign,
                h: toInt(match[HOUR]) * sign,
                m: toInt(match[MINUTE]) * sign,
                s: toInt(match[SECOND]) * sign,
                ms: toInt(match[MILLISECOND]) * sign
            };
        } else if (!!(match = isoDurationRegex.exec(input))) {
            sign = (match[1] === "-") ? -1 : 1;
            parseIso = function (inp) {
                // We'd normally use ~~inp for this, but unfortunately it also
                // converts floats to ints.
                // inp may be undefined, so careful calling replace on it.
                var res = inp && parseFloat(inp.replace(',', '.'));
                // apply sign while we're at it
                return (isNaN(res) ? 0 : res) * sign;
            };
            duration = {
                y: parseIso(match[2]),
                M: parseIso(match[3]),
                d: parseIso(match[4]),
                h: parseIso(match[5]),
                m: parseIso(match[6]),
                s: parseIso(match[7]),
                w: parseIso(match[8])
            };
        }

        ret = new Duration(duration);

        if (moment.isDuration(input) && input.hasOwnProperty('_lang')) {
            ret._lang = input._lang;
        }

        return ret;
    };

    // version number
    moment.version = VERSION;

    // default format
    moment.defaultFormat = isoFormat;

    // This function will be called whenever a moment is mutated.
    // It is intended to keep the offset in sync with the timezone.
    moment.updateOffset = function () {};

    // This function will load languages and then set the global language.  If
    // no arguments are passed in, it will simply return the current global
    // language key.
    moment.lang = function (key, values) {
        var r;
        if (!key) {
            return moment.fn._lang._abbr;
        }
        if (values) {
            loadLang(normalizeLanguage(key), values);
        } else if (values === null) {
            unloadLang(key);
            key = 'en';
        } else if (!languages[key]) {
            getLangDefinition(key);
        }
        r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
        return r._abbr;
    };

    // returns language data
    moment.langData = function (key) {
        if (key && key._lang && key._lang._abbr) {
            key = key._lang._abbr;
        }
        return getLangDefinition(key);
    };

    // compare moment object
    moment.isMoment = function (obj) {
        return obj instanceof Moment;
    };

    // for typechecking Duration objects
    moment.isDuration = function (obj) {
        return obj instanceof Duration;
    };

    for (i = lists.length - 1; i >= 0; --i) {
        makeList(lists[i]);
    }

    moment.normalizeUnits = function (units) {
        return normalizeUnits(units);
    };

    moment.invalid = function (flags) {
        var m = moment.utc(NaN);
        if (flags != null) {
            extend(m._pf, flags);
        }
        else {
            m._pf.userInvalidated = true;
        }

        return m;
    };

    moment.parseZone = function (input) {
        return moment(input).parseZone();
    };

    /************************************
        Moment Prototype
    ************************************/


    extend(moment.fn = Moment.prototype, {

        clone : function () {
            return moment(this);
        },

        valueOf : function () {
            return +this._d + ((this._offset || 0) * 60000);
        },

        unix : function () {
            return Math.floor(+this / 1000);
        },

        toString : function () {
            return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
        },

        toDate : function () {
            return this._offset ? new Date(+this) : this._d;
        },

        toISOString : function () {
            return formatMoment(moment(this).utc(), 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
        },

        toArray : function () {
            var m = this;
            return [
                m.year(),
                m.month(),
                m.date(),
                m.hours(),
                m.minutes(),
                m.seconds(),
                m.milliseconds()
            ];
        },

        isValid : function () {
            return isValid(this);
        },

        isDSTShifted : function () {

            if (this._a) {
                return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0;
            }

            return false;
        },

        parsingFlags : function () {
            return extend({}, this._pf);
        },

        invalidAt: function () {
            return this._pf.overflow;
        },

        utc : function () {
            return this.zone(0);
        },

        local : function () {
            this.zone(0);
            this._isUTC = false;
            return this;
        },

        format : function (inputString) {
            var output = formatMoment(this, inputString || moment.defaultFormat);
            return this.lang().postformat(output);
        },

        add : function (input, val) {
            var dur;
            // switch args to support add('s', 1) and add(1, 's')
            if (typeof input === 'string') {
                dur = moment.duration(+val, input);
            } else {
                dur = moment.duration(input, val);
            }
            addOrSubtractDurationFromMoment(this, dur, 1);
            return this;
        },

        subtract : function (input, val) {
            var dur;
            // switch args to support subtract('s', 1) and subtract(1, 's')
            if (typeof input === 'string') {
                dur = moment.duration(+val, input);
            } else {
                dur = moment.duration(input, val);
            }
            addOrSubtractDurationFromMoment(this, dur, -1);
            return this;
        },

        diff : function (input, units, asFloat) {
            var that = moment(input),
                zoneDiff = (this.zone() - that.zone()) * 6e4,
                diff, output;

            units = normalizeUnits(units);

            if (units === 'year' || units === 'month') {
                // average number of days in the months in the given dates
                diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
                // difference in months
                output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
                // adjust by taking difference in days, average number of days
                // and dst in the given months.
                output += ((this - moment(this).startOf('month')) -
                        (that - moment(that).startOf('month'))) / diff;
                // same as above but with zones, to negate all dst
                output -= ((this.zone() - moment(this).startOf('month').zone()) -
                        (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff;
                if (units === 'year') {
                    output = output / 12;
                }
            } else {
                diff = (this - that);
                output = units === 'second' ? diff / 1e3 : // 1000
                    units === 'minute' ? diff / 6e4 : // 1000 * 60
                    units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
                    units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
                    units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
                    diff;
            }
            return asFloat ? output : absRound(output);
        },

        from : function (time, withoutSuffix) {
            return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
        },

        fromNow : function (withoutSuffix) {
            return this.from(moment(), withoutSuffix);
        },

        calendar : function () {
            var diff = this.diff(moment().zone(this.zone()).startOf('day'), 'days', true),
                format = diff < -6 ? 'sameElse' :
                diff < -1 ? 'lastWeek' :
                diff < 0 ? 'lastDay' :
                diff < 1 ? 'sameDay' :
                diff < 2 ? 'nextDay' :
                diff < 7 ? 'nextWeek' : 'sameElse';
            return this.format(this.lang().calendar(format, this));
        },

        isLeapYear : function () {
            return isLeapYear(this.year());
        },

        isDST : function () {
            return (this.zone() < this.clone().month(0).zone() ||
                this.zone() < this.clone().month(5).zone());
        },

        day : function (input) {
            var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
            if (input != null) {
                input = parseWeekday(input, this.lang());
                return this.add({ d : input - day });
            } else {
                return day;
            }
        },

        month : function (input) {
            var utc = this._isUTC ? 'UTC' : '',
                dayOfMonth;

            if (input != null) {
                if (typeof input === 'string') {
                    input = this.lang().monthsParse(input);
                    if (typeof input !== 'number') {
                        return this;
                    }
                }

                dayOfMonth = this.date();
                this.date(1);
                this._d['set' + utc + 'Month'](input);
                this.date(Math.min(dayOfMonth, this.daysInMonth()));

                moment.updateOffset(this);
                return this;
            } else {
                return this._d['get' + utc + 'Month']();
            }
        },

        startOf: function (units) {
            units = normalizeUnits(units);
            // the following switch intentionally omits break keywords
            // to utilize falling through the cases.
            switch (units) {
            case 'year':
                this.month(0);
                /* falls through */
            case 'month':
                this.date(1);
                /* falls through */
            case 'week':
            case 'isoWeek':
            case 'day':
                this.hours(0);
                /* falls through */
            case 'hour':
                this.minutes(0);
                /* falls through */
            case 'minute':
                this.seconds(0);
                /* falls through */
            case 'second':
                this.milliseconds(0);
                /* falls through */
            }

            // weeks are a special case
            if (units === 'week') {
                this.weekday(0);
            } else if (units === 'isoWeek') {
                this.isoWeekday(1);
            }

            return this;
        },

        endOf: function (units) {
            units = normalizeUnits(units);
            return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1);
        },

        isAfter: function (input, units) {
            units = typeof units !== 'undefined' ? units : 'millisecond';
            return +this.clone().startOf(units) > +moment(input).startOf(units);
        },

        isBefore: function (input, units) {
            units = typeof units !== 'undefined' ? units : 'millisecond';
            return +this.clone().startOf(units) < +moment(input).startOf(units);
        },

        isSame: function (input, units) {
            units = typeof units !== 'undefined' ? units : 'millisecond';
            return +this.clone().startOf(units) === +moment(input).startOf(units);
        },

        min: function (other) {
            other = moment.apply(null, arguments);
            return other < this ? this : other;
        },

        max: function (other) {
            other = moment.apply(null, arguments);
            return other > this ? this : other;
        },

        zone : function (input) {
            var offset = this._offset || 0;
            if (input != null) {
                if (typeof input === "string") {
                    input = timezoneMinutesFromString(input);
                }
                if (Math.abs(input) < 16) {
                    input = input * 60;
                }
                this._offset = input;
                this._isUTC = true;
                if (offset !== input) {
                    addOrSubtractDurationFromMoment(this, moment.duration(offset - input, 'm'), 1, true);
                }
            } else {
                return this._isUTC ? offset : this._d.getTimezoneOffset();
            }
            return this;
        },

        zoneAbbr : function () {
            return this._isUTC ? "UTC" : "";
        },

        zoneName : function () {
            return this._isUTC ? "Coordinated Universal Time" : "";
        },

        parseZone : function () {
            if (typeof this._i === 'string') {
                this.zone(this._i);
            }
            return this;
        },

        hasAlignedHourOffset : function (input) {
            if (!input) {
                input = 0;
            }
            else {
                input = moment(input).zone();
            }

            return (this.zone() - input) % 60 === 0;
        },

        daysInMonth : function () {
            return daysInMonth(this.year(), this.month());
        },

        dayOfYear : function (input) {
            var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
            return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
        },

        quarter : function () {
            return Math.ceil((this.month() + 1.0) / 3.0);
        },

        weekYear : function (input) {
            var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year;
            return input == null ? year : this.add("y", (input - year));
        },

        isoWeekYear : function (input) {
            var year = weekOfYear(this, 1, 4).year;
            return input == null ? year : this.add("y", (input - year));
        },

        week : function (input) {
            var week = this.lang().week(this);
            return input == null ? week : this.add("d", (input - week) * 7);
        },

        isoWeek : function (input) {
            var week = weekOfYear(this, 1, 4).week;
            return input == null ? week : this.add("d", (input - week) * 7);
        },

        weekday : function (input) {
            var weekday = (this.day() + 7 - this.lang()._week.dow) % 7;
            return input == null ? weekday : this.add("d", input - weekday);
        },

        isoWeekday : function (input) {
            // behaves the same as moment#day except
            // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
            // as a setter, sunday should belong to the previous week.
            return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
        },

        get : function (units) {
            units = normalizeUnits(units);
            return this[units]();
        },

        set : function (units, value) {
            units = normalizeUnits(units);
            if (typeof this[units] === 'function') {
                this[units](value);
            }
            return this;
        },

        // If passed a language key, it will set the language for this
        // instance.  Otherwise, it will return the language configuration
        // variables for this instance.
        lang : function (key) {
            if (key === undefined) {
                return this._lang;
            } else {
                this._lang = getLangDefinition(key);
                return this;
            }
        }
    });

    // helper for adding shortcuts
    function makeGetterAndSetter(name, key) {
        moment.fn[name] = moment.fn[name + 's'] = function (input) {
            var utc = this._isUTC ? 'UTC' : '';
            if (input != null) {
                this._d['set' + utc + key](input);
                moment.updateOffset(this);
                return this;
            } else {
                return this._d['get' + utc + key]();
            }
        };
    }

    // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
    for (i = 0; i < proxyGettersAndSetters.length; i ++) {
        makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]);
    }

    // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
    makeGetterAndSetter('year', 'FullYear');

    // add plural methods
    moment.fn.days = moment.fn.day;
    moment.fn.months = moment.fn.month;
    moment.fn.weeks = moment.fn.week;
    moment.fn.isoWeeks = moment.fn.isoWeek;

    // add aliased format methods
    moment.fn.toJSON = moment.fn.toISOString;

    /************************************
        Duration Prototype
    ************************************/


    extend(moment.duration.fn = Duration.prototype, {

        _bubble : function () {
            var milliseconds = this._milliseconds,
                days = this._days,
                months = this._months,
                data = this._data,
                seconds, minutes, hours, years;

            // The following code bubbles up values, see the tests for
            // examples of what that means.
            data.milliseconds = milliseconds % 1000;

            seconds = absRound(milliseconds / 1000);
            data.seconds = seconds % 60;

            minutes = absRound(seconds / 60);
            data.minutes = minutes % 60;

            hours = absRound(minutes / 60);
            data.hours = hours % 24;

            days += absRound(hours / 24);
            data.days = days % 30;

            months += absRound(days / 30);
            data.months = months % 12;

            years = absRound(months / 12);
            data.years = years;
        },

        weeks : function () {
            return absRound(this.days() / 7);
        },

        valueOf : function () {
            return this._milliseconds +
              this._days * 864e5 +
              (this._months % 12) * 2592e6 +
              toInt(this._months / 12) * 31536e6;
        },

        humanize : function (withSuffix) {
            var difference = +this,
                output = relativeTime(difference, !withSuffix, this.lang());

            if (withSuffix) {
                output = this.lang().pastFuture(difference, output);
            }

            return this.lang().postformat(output);
        },

        add : function (input, val) {
            // supports only 2.0-style add(1, 's') or add(moment)
            var dur = moment.duration(input, val);

            this._milliseconds += dur._milliseconds;
            this._days += dur._days;
            this._months += dur._months;

            this._bubble();

            return this;
        },

        subtract : function (input, val) {
            var dur = moment.duration(input, val);

            this._milliseconds -= dur._milliseconds;
            this._days -= dur._days;
            this._months -= dur._months;

            this._bubble();

            return this;
        },

        get : function (units) {
            units = normalizeUnits(units);
            return this[units.toLowerCase() + 's']();
        },

        as : function (units) {
            units = normalizeUnits(units);
            return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's']();
        },

        lang : moment.fn.lang,

        toIsoString : function () {
            // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
            var years = Math.abs(this.years()),
                months = Math.abs(this.months()),
                days = Math.abs(this.days()),
                hours = Math.abs(this.hours()),
                minutes = Math.abs(this.minutes()),
                seconds = Math.abs(this.seconds() + this.milliseconds() / 1000);

            if (!this.asSeconds()) {
                // this is the same as C#'s (Noda) and python (isodate)...
                // but not other JS (goog.date)
                return 'P0D';
            }

            return (this.asSeconds() < 0 ? '-' : '') +
                'P' +
                (years ? years + 'Y' : '') +
                (months ? months + 'M' : '') +
                (days ? days + 'D' : '') +
                ((hours || minutes || seconds) ? 'T' : '') +
                (hours ? hours + 'H' : '') +
                (minutes ? minutes + 'M' : '') +
                (seconds ? seconds + 'S' : '');
        }
    });

    function makeDurationGetter(name) {
        moment.duration.fn[name] = function () {
            return this._data[name];
        };
    }

    function makeDurationAsGetter(name, factor) {
        moment.duration.fn['as' + name] = function () {
            return +this / factor;
        };
    }

    for (i in unitMillisecondFactors) {
        if (unitMillisecondFactors.hasOwnProperty(i)) {
            makeDurationAsGetter(i, unitMillisecondFactors[i]);
            makeDurationGetter(i.toLowerCase());
        }
    }

    makeDurationAsGetter('Weeks', 6048e5);
    moment.duration.fn.asMonths = function () {
        return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12;
    };


    /************************************
        Default Lang
    ************************************/


    // Set default language, other languages will inherit from English.
    moment.lang('en', {
        ordinal : function (number) {
            var b = number % 10,
                output = (toInt(number % 100 / 10) === 1) ? 'th' :
                (b === 1) ? 'st' :
                (b === 2) ? 'nd' :
                (b === 3) ? 'rd' : 'th';
            return number + output;
        }
    });

    /* EMBED_LANGUAGES */

    /************************************
        Exposing Moment
    ************************************/

    function makeGlobal(deprecate) {
        var warned = false, local_moment = moment;
        /*global ender:false */
        if (typeof ender !== 'undefined') {
            return;
        }
        // here, `this` means `window` in the browser, or `global` on the server
        // add `moment` as a global object via a string identifier,
        // for Closure Compiler "advanced" mode
        if (deprecate) {
            global.moment = function () {
                if (!warned && console && console.warn) {
                    warned = true;
                    console.warn(
                            "Accessing Moment through the global scope is " +
                            "deprecated, and will be removed in an upcoming " +
                            "release.");
                }
                return local_moment.apply(null, arguments);
            };
            extend(global.moment, local_moment);
        } else {
            global['moment'] = moment;
        }
    }

    // CommonJS module is defined
    if (hasModule) {
        module.exports = moment;
    }
    makeGlobal();

}).call(this);

(function(root, factory) {
  //call factory to create t8 object
  var lib = factory(t8, root['numeral']);
  //set up as global variable for browsers, rhino and node
  root['t8'] = lib;
  //set up as export for node
  if (typeof exports !== 'undefined') {
    module.exports = lib;
  }
}(this, function(t8, numeral) {
  'use strict';

  var CurrencyFormatter = t8.CurrencyFormatter = function() {};

  var lang = {
    'ar_AE' : 'ar',
    'cs_CZ' : 'cs',
    'da_DK' : 'da-dk',
    'de_DE' : 'de',
    'en_US' : 'en',
    'es_ES' : 'es',
    'fr_FR' : 'fr',
    'in_ID' : 'in-id',
    'it_IT' : 'it',
    'ja_JP' : 'ja',
    'ko_KR' : 'ko-kr',
    'ms_MY' : 'ms-my',
    'nl_NL' : 'nl-nl',
    'no_NO' : 'no-no',
    'pl_PL' : 'pl',
    'pt_BR' : 'pt-br',
    'ro_RO' : 'ro-ro',
    'ru_RU' : 'ru',
    'sv_SE' : 'sv-se',
    'th_TH' : 'th',
    'tl_PH' : 'tl-ph',
    'tr_TR' : 'tr',
    'zh_CN' : 'zh-cn',
    'zh_TW' : 'zh-tw'
  };

  CurrencyFormatter.prototype.format = function(amount, currency, locale) {
    if(typeof lang[locale] !== 'undefined'){
      numeral.language(lang[locale]);
    } else {
      numeral.language('en');
    }

    return numeral(amount).formatCurrency('full', currency);
  };

  return t8;
}));
/**
 * Given a piece of text (as a date) and a locale, make the date render in the correct format of that locale.
 * The goal here is to migrate date formatter from Java into JavaScript
 */

/**********************
* DEPENDENCIES
*****
We are using the open-source plugin:
[] moment.js
[] => http://momentjs.com/
[] => https://github.com/moment/moment/
[] => moment.js has been forked and a new version is being developed, which complies more with CLDR standards
[]
*****/
(function(root, factory) {
  //call factory to create t8 object
  var lib = factory(t8, root['moment']);

  //set up as global variable for browsers, rhino and node
  root['t8'] = lib;
  //set up as export for node
  if (typeof exports !== 'undefined') {
    module.exports = lib;
  }
}(this, function(t8, moment) {
  "use strict";

  var DateFormatter = t8.DateFormatter = function() {};
  var Utils = t8.Utils;

  DateFormatter.prototype.format = function(date, locale, pattern, useTimeZone) {
    /*
    moment.js handles the bulk of the heavy lifting here; this format function acts more as a bridge between the existing test specs
    and what moment.js needs in order to parse it correctly.
    */

    /* moment.js parses all locales as lowercase strings, so let's convert it */
    locale = locale.replace("_", "-").toLowerCase();

    /*
    Moment uses a 2-character locale identification for most locales, 
    EXCEPT the following (where it uses the full xx-xx format or close therein) 
    */
    var localesFullName = ['ar-ma','en-au','en-ca','en-gb','fr-ca','ms-my',
                          'pt-br','tl-ph','tzm-la','tzm','zh-cn','zh-tw'];

    /* we're always dealing with exceptions; is this locale one of those exceptions? */
    if(!Utils.contains(localesFullName, locale)) {

      // the java implementation needs the norwegian locale to be no_no, moment needs it to be nn_no;
      // to preserve existing unit tests for java (to pass) and moment (to pass), this small hack is inserted...
      // though i'd rather just switch it to nn_NO permantently and scrap the java implementation...
      // NOTE: this also applies to in_ID, which should be id_ID (but changing a locale name could have far-reaching impacts)
      locale = locale.replace('no', 'nn').replace('in','id').substring(0, 2);
    }

    /* needing iso format support, add a check before the regex check */
    if(pattern === "iso") {
      return moment(date).toISOString();
    }

    /* the original pattern from the test spec had patterns that moment.js doesn't support,
     * so we perform some replacements of the pattern to keep in line with moment.js
    */
    var regexEscaped = /('.*?')+?/g,        /* escaped text in our test spec uses ''. moment.js uses []. convert this first */
        regexBridgeYear = /y/g,             /* short year is YY, long year is YYYY. no more lowercase y in moment.js. */
        regexBridgeDoW = /d{2}/,            /* lowercase dd=abbreviated day of the week (Mo, Tu) in moment; convert to uppercase DD to be day of month (01 02 ... 31) */
        regexBridgeDoM = /d{1}(?![\w])/,    /* lowercase d=number day of the week (0-6) in moment; convert to uppercase D to be day of month (1 2 ... 31) */
        regexBridgeWeekday = /EEEE/g,       /* in moment.js, day of the week (eg. Friday) is dddd, 3 letter (Fri) is ddd, 2 letter (Fr) is dd */
        regexBridgeAM = /a{1,3}/i;          /* in moment.js, A=AM/PM or a=am/pm. no aaa for uppercase */

    pattern = pattern
              .replace(regexEscaped, function(match, p, offset, str) {
                return "["+match.substring(1, match.length-1)+"]";
              })
              .replace(regexBridgeYear, function(match) {
                return match.toUpperCase();
              })
              .replace(regexBridgeDoW, "DD")
              .replace(regexBridgeDoM, "D")
              .replace(regexBridgeWeekday, "dddd")
              .replace(regexBridgeAM,"A");

    /* if the format requires parsing the data in the local timezone, don't parse the date using UTC through moment */
    if(useTimeZone) {
      return moment(date).lang(locale).format(pattern);
    }

    return moment(date).utc().lang(locale).format(pattern);
  };

  return t8;
}));
(function(root, factory) {
  //call factory to create t8 object
  var lib = factory(t8, root['numeral']);

  //set up as global variable for browsers, rhino and node
  root['t8'] = lib;
  //set up as export for node
  if (typeof exports !== 'undefined') {
    module.exports = lib;
  }
}(this, function(t8, numeral) {
  'use strict';

  var NumberFormatter = t8.NumberFormatter = function() {};

  var lang = {
    'ar_AE' : 'ar-ae',
    'cs_CZ' : 'cs',
    'da_DK' : 'da-dk',
    'de_DE' : 'de',
    'en_US' : 'en',
    'es_ES' : 'es',
    'fr_FR' : 'fr',
    'in_ID' : 'in-id',
    'it_IT' : 'it',
    'ja_JP' : 'ja',
    'ko_KR' : 'ko-kr',
    'ms_MY' : 'ms-my',
    'nl_NL' : 'nl-nl',
    'no_NO' : 'no-no',
    'pl_PL' : 'pl',
    'pt_BR' : 'pt-br',
    'ro_RO' : 'ro-ro',
    'ru_RU' : 'ru',
    'sv_SE' : 'sv-se',
    'th_TH' : 'th',
    'tl_PH' : 'tl-ph',
    'tr_TR' : 'tr',
    'zh_CN' : 'zh-cn',
    'zh_TW' : 'zh-tw'
  };

  NumberFormatter.prototype.format = function(value, locale) {

    if(typeof lang[locale] !== 'undefined'){
      numeral.language(lang[locale]);
    } else {
      numeral.language('en');
    }

    return numeral(value).format();
  };

  return t8;
}));
// Simple JavaScript Templating
// John Resig - http://ejohn.org/ - MIT Licensed
(function(){
  var cache = {};

  this.tmpl = function tmpl(str, data){
    // Figure out if we're getting a template, or if we need to
    // load the template - and be sure to cache the result.
    var fn = !/\W/.test(str) ?
      cache[str] = cache[str] ||
        tmpl(document.getElementById(str).innerHTML) :
      // Generate a reusable function that will serve as a template
      // generator (and which will be cached).
      new Function("obj",
        "var p=[],print=function(){p.push.apply(p,arguments);};" +

        // Introduce the data as local variables using with(){}
        "with(obj){p.push('" +

        // Convert the template into pure JavaScript
        str
          .replace(/[\r\t\n]/g, " ")
          .split("<%").join("\t")
          .replace(/((^|%>)[^\t]*)'/g, "$1\r")
          .replace(/\t=(.*?)%>/g, "',$1,'")
          .split("\t").join("');")
          .split("%>").join("p.push('")
          .split("\r").join("\\'")
      + "');}return p.join('');");

    // Provide some basic currying to the user
    return data ? fn( data ) : fn;
  };
})();
(function(root, factory) {
  //call factory to create t8 object
  var lib = factory(t8);

  //set up as global variable for browsers, rhino and node
  root['t8'] = lib;
  //set up as export for node
  if (typeof exports !== 'undefined') {
    module.exports = lib;
  }
}(this, function(t8) {
  'use strict';

  var NameFormatter = t8.NameFormatter = function() {};
  var Utils = t8.Utils;

  NameFormatter.formats = {
    FAMILIAR_NAME : 'FAMILIAR_NAME',
    FULL_NAME     : 'FULL_NAME',
    MICROFORMAT   : 'MICROFORMAT',
    LIST_VIEW     : 'LIST_VIEW'
  };

  NameFormatter.templates = {
    MICROFORMAT : {
      firstName  : tmpl('<span class="given-name"><%=value%></span>'),
      lastName   : tmpl('<span class="family-name"><%=value%></span>'),
      maidenName : tmpl('<span class="additional-name"><%=value%></span>')
    },
    FAMILIAR_NAME : {
      defaultTemplate : tmpl('<%=firstName%>'),
      localeTemplates : [
        {
          locales     : ['de_DE', 'nl_NL','pl_PL','ro_RO', 'tr_TR'],
          template    : tmpl('<%=firstName%> <%=lastName%>')
        },
        {
          locales     : ['CJK'],
          template    : tmpl('<%=lastName%><%=firstName%>')
        },
        {
          locales     : ['CJK-ja_JP'],
          template    : tmpl('<%=lastName%> <%=firstName%>')
        }
      ]
    },
    FULL_NAME : {
      defaultTemplate : tmpl('<%=firstName%><%if(maidenName){%> (<%=maidenName%>)<%}%> <%=lastName%>'),
      localeTemplates : [
        {
          locales     : ['ar_AE', 'th_TH'],
          template    : tmpl('<%=firstName%><%if(maidenName){%> <%=maidenName%><%}%><%if(lastName){%> <%=lastName%><%}%>')
        },
        {
          locales     : ['cs_CZ'],
          template    : tmpl('<%=firstName%><%if(lastName){%> <%=lastName%><%}%><%if(maidenName){%> (roz. <%=maidenName%>)<%}%>')
        },
        {
          locales     : ['de_DE'],
          template    : tmpl('<%=firstName%><%if(lastName){%> <%=lastName%><%}%><%if(maidenName){%> geb. <%=maidenName%><%}%>')
        },
        {
          locales     : ['CJK-ja_JP'],
          template    : tmpl('<%=lastName%><%if(firstName){%> <%=firstName%><%}%><%if(maidenName){%> (<%=maidenName%>)<%}%>')
        },
        {
          locales     : ['CJK'],
          template    : tmpl('<%=lastName%><%=firstName%><%if(maidenName){%> (<%=maidenName%>)<%}%>')
        },
        {
          locales     : ['ms_MY'],
          template    : tmpl('<%=firstName%><%if(lastName){%> <%=lastName%><%}%><%if(maidenName){%> (<%=maidenName%>)<%}%>')
        },
        {
          locales     : ['nl_NL'],
          template    : tmpl('<%=firstName%> <%=lastName%><%if(maidenName){%>-<%=maidenName%><%}%>')
        },
        {
          locales     : ['pl_PL'],
          template    : tmpl('<%=firstName%><%if(lastName){%> <%=lastName%><%}%><%if(maidenName){%> z d. <%=maidenName%><%}%>')
        }
      ]
    },
    LIST_VIEW: {
      defaultTemplate : tmpl('<%if(lastName){%><%=lastName%>, <%}%><%=firstName%>'),
      localeTemplates : [
        {
          locales     : ['CJK'],
          template    : tmpl('<%=lastName%><%=firstName%>')
        },
        {
          locales     : ['CJK-ja_JP'],
          template    : tmpl('<%=lastName%> <%=firstName%>')
        },
        {
          locales     : ['ar_AE', 'in_ID', 'ms_MY', 'th_TH'],
          template    : tmpl('<%=firstName%> <%=lastName%>')
        }
      ]
    }
  };

  NameFormatter.locales = {
    CJK : 'CJK',
    CJK_ja_JP : 'CJK-ja_JP',
    ja_JP : 'ja_JP'
  };

  NameFormatter.charsets = {
    //Korean uAC00-uD7AF
    korean : {
      //uAC00: parseInt('AC00', 16) = 44032
      lowerbound : 44032,
      //uD7AF: parseInt('D7AF', 16) = 55215
      upperbound : 55215
    },
    CJ : [
      //Japanese Kanji char set u4E00--u9FBF : Common to Japanese and Chinese
      {
        //u4E00: parseInt('4E00', 16) = 19968
        lowerbound : 19968,
        //u9FBF: parseInt('9FBF', 16) = 40895
        upperbound : 40895
      },
      //Japanese Katakana u30A0 - u30FF
      {
        //u30A0: parseInt('30A0', 16) = 12448
        lowerbound : 12448,
        //u30FF: parseInt('30FF', 16) = 12543
        upperbound : 12543
      },
      //Japanese Katakana (Half Width): uFF61 - uFF9F
      {
        //uFF61: parseInt('FF61', 16) = 65377
        lowerbound : 65377,
        //uFF9F: parseInt('FF9F', 16) = 65439
        upperbound : 65439
      },
      //Chinese simplified: GB 2312
      {
        //u3040: parseInt('3040', 16) = 12352
        lowerbound : 12352,
        //u309F: parseInt('309F', 16) = 12447
        upperbound : 12447
      }
    ]
  };

  NameFormatter.prototype.htmlEncode = function(str) {
    if((str === null) || (str === undefined)) {
      return null;
    }

    return str.toString().replace(/(.)/g, function(a) {
      if (a === '<') {
        return '&lt;';
      } else if (a === '>') {
        return '&gt;';
      } else if (a === '&') {
          return '&amp;';
      } else if (a === '"') {
        return '&quot;';
      } else {
        if (a.charCodeAt(0) < 127) {
          return a;
        }
        return '&#x' + a.charCodeAt(0).toString(16).toLowerCase() + ';';
      }
    });
  };

  NameFormatter.prototype.format = function(name, format, locale) {

    function isKoreanCharset(lastName) {
      if (!lastName) {
        return false;
      }
      var koreanCharset = NameFormatter.charsets.korean,
        firstCharCode = lastName.charCodeAt(0);
      return firstCharCode >= koreanCharset.lowerbound && firstCharCode <= koreanCharset.upperbound;
    }

    function isCJCharset(lastName) {
      if (!lastName) {
        return false;
      }
      var firstCharCode = lastName.charCodeAt(0);
      return Utils.some(NameFormatter.charsets.CJ, function(charset) {
        return firstCharCode >= charset.lowerbound && firstCharCode >= charset.upperbound;
      });
    }

    function getLocaleTemplate(formatTemplates, locale) {
      var localeTemplate = Utils.find(formatTemplates.localeTemplates, function(currLocaleTemplate) {
        return Utils.indexOf(currLocaleTemplate.locales, locale) >= 0;
      });
      return localeTemplate ? localeTemplate.template : formatTemplates.defaultTemplate;
    }

    function getFormatTemplates(format) {
      var formats = NameFormatter.formats,
        templates = NameFormatter.templates;

      if (!format) {
        return templates.FAMILIAR_NAME;
      }

      if (Utils.isString(format)) {
        format = [format];
      }

      if (! Utils.isArray(format)) {
        return templates.FAMILIAR_NAME;
      }
      if (Utils.indexOf(format, formats.FULL_NAME) >= 0) {
          return templates.FULL_NAME;
      }
      if (Utils.indexOf(format, formats.LIST_VIEW ) >= 0) {
          return templates.LIST_VIEW;
      }
      return templates.FAMILIAR_NAME;
    }

    function hasMicroformat(format) {
      if (format) {
        if (Utils.isArray(format)) {
          return (Utils.indexOf(format, NameFormatter.formats.MICROFORMAT) >=0);
        } else if (Utils.isString(format)) {
          return format === NameFormatter.formats.MICROFORMAT;
        }
      }
      return false;
    }

    function processInputValue(value, isHTMLOutput, useMicroformat, htmlEncode, microformatTemplate) {
      if (!value) {
        return '';
      }

      //strip leading and trailing whitespace
      var processedValue = value.replace(TRIM_WHITESPACE_REGEX,'');
      //html encode if expected output is HTML
      if (isHTMLOutput) {
        processedValue = htmlEncode(processedValue);
      }
      //apply microformat template if MICROFORMAT format is requested
      if (useMicroformat) {
        processedValue = microformatTemplate({ value: processedValue });
      }
      return processedValue;
    }

    var TRIM_WHITESPACE_REGEX = /(^\s+|\s+$)/g,
      useMicroformat        = hasMicroformat(format),
      isHTMLOutput          = (useMicroformat || name.lastNameWithHighlight),
      microformatTemplates  = NameFormatter.templates.MICROFORMAT,
      firstName             = processInputValue(name.firstName, isHTMLOutput, useMicroformat, this.htmlEncode, microformatTemplates.firstName),
      lastName              = processInputValue(name.lastName,  isHTMLOutput, useMicroformat, this.htmlEncode, microformatTemplates.lastName),
      maidenName            = processInputValue(name.maidenName,isHTMLOutput, useMicroformat, this.htmlEncode, microformatTemplates.maidenName),
      //do not html encode lastNameWithHighlight because it was intentionally formatted with markup and that markup needs to be preserved
      lastNameWithHighlight = processInputValue(name.lastNameWithHighlight, false, useMicroformat, this.htmlEncode, microformatTemplates.lastName),
      formatTemplate        = '',
      nameFormatted         = '';

    //if last name starts with a CJK (chinese, japanese or korean) character we need to use CJK name format
    //looking at the non-processed value of last name to determine character set
    //(in order to maintain same behavior as JAVA version of name formatter)
    if (isKoreanCharset(name.lastName)) {
      //if it's korean character always use CJK format
      locale = NameFormatter.locales.CJK;
    } else if (isCJCharset(name.lastName)) {
      //if it's chinese or japanese and locale is ja_JP need to use special japanese display format
      if (locale === NameFormatter.locales.ja_JP) {
        locale = NameFormatter.locales.CJK_ja_JP;
      } else {
        //otherwise use standard CJK format
        locale = NameFormatter.locales.CJK;
      }
    }

    //get the template string we'll use to format the name
    formatTemplate = getLocaleTemplate(getFormatTemplates(format), locale);

    //format the name according to requested format
    nameFormatted = formatTemplate({
      firstName: firstName,
      lastName: lastNameWithHighlight? lastNameWithHighlight : lastName,
      maidenName: maidenName
    });

    //remove any trailing and leading whitespaces in resulting name
    nameFormatted = nameFormatted.replace(TRIM_WHITESPACE_REGEX,'');

    return nameFormatted;
  };

  return t8;
}));

(function(root, factory) {
  //call factory to create t8 object
  var lib = factory(t8);

  //set up as global variable for browsers, rhino and node
  root['t8'] = lib;
  //set up as export for node
  if (typeof exports !== 'undefined') {
    module.exports = lib;
  }
}(this, function(t8) {
  'use strict';

  var _ellipsis = '...';

  var Truncator = t8.TruncationFormatter = function(ellipsis) {
    _ellipsis = typeof(ellipsis) !== 'undefined' ? ellipsis : _ellipsis;
  };

  /**
   * Returns the string truncated at the size if it is bigger.
   * @param value {string}
   * @param limit {integer}
   * @return substring of original {string}, truncated at the specified limit
   */
  Truncator.prototype.format = function(value, limit) {
    if(!value || typeof value !== 'string') {
      // this is to help maintain functional parity
      if(typeof(value) === 'undefined' || value === '') {
        return '';
      }
      return null;
    }
    // this is to help maintain functional parity
    if(typeof limit === 'undefined') {
      return '...';
    }

    if(!limit ||
      typeof limit !== 'number' ||
      limit >= value.length ||
      limit < 0 ||
      value.replace(/\s/g,'').length === 0) {
      return value;
    }

    var truncated = value.substr(0, limit),
      chars = truncated.split(''),
      idx = limit - 1,
      output = '',
      regexPunctuation = /\s|\?|\!|\.|\,|\;|\:/g; // whitespace or punctuation regex

    while(idx >= 0) {
      if(!regexPunctuation.test(chars[idx])) {
        idx--;
      } else {
        break;
      }
    }

    if(idx > 0) {
      output = truncated.substr(0, idx);
    } else {
      // case where we couldn't find a single white space
      output = truncated;
    }

    output += _ellipsis;

    return output;
  };
  return t8;
}));
(function(root, factory) {
  //call factory to create t8 object
  var lib = factory(t8);

  //set up as global variable for browsers, rhino and node
  root['t8'] = lib;
  //set up as export for node
  if (typeof exports !== 'undefined') {
    module.exports = lib;
  }
}(this, function(t8) {
  t8.GrammarRules = t8.GrammarRules ? t8.GrammarRules : {};
  t8.GrammarRules.Possessive = t8.GrammarRules.Possessive ? t8.GrammarRules.Possessive : {};
  t8.GrammarRules.Possessive["default"] = {};
  return t8;
}));
(function(root, factory) {
  //call factory to create t8 object
  var lib = factory(t8);

  //set up as global variable for browsers, rhino and node
  root['t8'] = lib;
  //set up as export for node
  if (typeof exports !== 'undefined') {
    module.exports = lib;
  }
}(this, function(t8) {
  t8.GrammarRules.Possessive['en_US'] = {
    "fallback": "'s",
    "rules": {
      ".*[Ss]$": "'",
      ".*[A-RT-Z]$": "'S",
      ".*[a-rt-z]$": "'s"
    }
  };
  return t8;
}));
/**
 * Given a piece of text and a locale, make the text possessive using the proper rules for the locale. This code
 * depends on src/formatters/possessive-grammar/*.js for the language-specific possessive definitions.
 *
 * Example: new t8.Possessive().format("Jim", "en_US")
 *
 * Output: Jim's
 *
 */
(function(root, factory) {
  //call factory to create t8 object
  var lib = factory(t8);

  //set up as global variable for browsers, rhino and node
  root['t8'] = lib;
  //set up as export for node
  if (typeof exports !== 'undefined') {
    module.exports = lib;
  }
}(this, function(t8) {
  'use strict';

  var Possessive = t8.Possessive = function() {},
   Utils = t8.Utils,
   PossessiveGrammar = t8.GrammarRules.Possessive;

  Possessive.prototype.format = function(text, locale) {
    var possessiveRules = PossessiveGrammar[locale];

    if (possessiveRules) {
      var suffixFromRules = Utils.find(possessiveRules.rules, function(suffix, regexString) {
        var regex = new RegExp(regexString);
        return regex.test(text);
      });

      if (Utils.isDefined(suffixFromRules)) {
        return text + suffixFromRules;
      } else if (possessiveRules.fallback) {
        return text + possessiveRules.fallback;
      }
    }

    return text;
  };

  return t8;
}));
(function(root, factory) {
  //call factory to create t8 object
  var lib = factory(t8);
  //set up as global variable for browsers, rhino and node
  root['t8'] = lib;
  //set up as export for node
  if (typeof exports !== 'undefined') {
    module.exports = lib;
  }
}(this, function(t8) {
  var SINGULAR = 0,
      PLURAL = 1,
      DUAL = 2,
      FEW = 3,
      MANY = 4,
      ZERO = 5;

  t8.GrammarRules = t8.GrammarRules ? t8.GrammarRules : {};
  t8.GrammarRules.Chooser = t8.GrammarRules.Chooser ? t8.GrammarRules.Chooser : {};
  t8.GrammarRules.Chooser["default"] = {
    'equals': {
      '1': SINGULAR
    },
    'endsWith': {
      '0': PLURAL,
      '1': PLURAL,
      '2': PLURAL,
      '3': PLURAL,
      '4': PLURAL,
      '5': PLURAL,
      '6': PLURAL,
      '7': PLURAL,
      '8': PLURAL,
      '9': PLURAL
    }
  };
  t8.GrammarRules.Chooser.longestEndsWith = 2;
  // 0 = singular, 1 = plural, 2 = dual, 3 = few, 4 = many, 5 = zero
  t8.GrammarRules.Chooser.categories = [
    'singular',
    'plural',
    'dual',
    'few',
    'many',
    'zero'
  ];
  return t8;
}));
/**
 * Given a list of chooser rules and a value, pick the rule that best matches the value for the current locale. This is
 * usually used for picking the proper singular/plural text based on some numeric value.
 *
 * This code depends on src/i18n/chooser-grammar/*.js files for the language-specific category definitions.
 *
 * There are two patterns of usage.
 *
 * Pattern one: purely numeric
 * ===========================
 *
 * In this pattern, all args are numbers and are treated as ranges.
 *
 * Example rules:
 *
 * [
 *   {
 *     "arg": 0,
 *     "comparison": "gte",
 *     "text": "zero"
 *   },
 *   {
 *     "arg": 2,
 *     "comparison": "gte",
 *     "text": "two"
 *   },
 *   {
 *     "arg": 20,
 *     "comparison": "gt",
 *     "text": "more than 20"
 *   }
 * ]
 *
 * Each rule is an object with an "arg" field that has a numeric value, a comparison type (gt = greater than, gte =
 * greater than or equal), and the text to use if that object is selected. The chooser will return the text for the
 * object with the largest arg that is less than the given value.
 *
 * For the example above:
 *
 *   1. A value of 0 returns "zero"
 *   2. Values 2 - 20 returns "two"
 *   3. Any value greater than 20 returns "more than 20"
 *
 *
 * Pattern 2: categories
 * ===========================
 *
 * In this pattern, in addition to numbers, you can use categories like "singular" and "plural" that are pre-defined
 * for every locale. Note that numbers here are treated as exact matches and not ranges.
 *
 * Example rules:
 *
 * [
 *   {
 *     "category": "singular",
 *     "comparison": "eq",
 *     "text": "connection"
 *   },
 *   {
 *     "category": "plural",
 *     "comparison": "eq",
 *     "text": "connections"
 *   }
 * ]
 *
 * Here, the rule selected depends on the grammar rules defined for the current locale in src/i18n/chooser-grammar/[locale].js file.
 * Note that numbers can also be included in the list of rules, but as soon as you have a single category, numbers are
 * treated as exact matches and not ranges.
 *
 */
(function(root, factory) {
  //call factory to create t8 object
  var lib = factory(t8);
  //set up as global variable for browsers, rhino and node
  root['t8'] = lib;
  //set up as export for node
  if (typeof exports !== 'undefined') {
    module.exports = lib;
  }
}(this, function(t8) {
  'use strict';

  var Chooser = t8.Chooser = function() {};

  var Utils = t8.Utils,
      isDefined = Utils.isDefined,
      grammarRules = t8.GrammarRules.Chooser;

  Chooser.COMPARISONS = {
    eq: function(left, right) { return left === right; },
    gt: function(left, right) { return left > right; },
    gte: function(left, right) { return left >= right; },
    endsWith: function(left, right) { return left.toString().endsWith(right.toString()); }
  };

  Chooser.prototype.findRule = function(rules, value, comparison) {
    return Utils.find(rules, function(rule) {
      return isDefined(Utils.find(rule.values, function(v) { return comparison(value, v); }));
    });
  };

  Chooser.prototype.pickCategory = function(gramma, value, longestEndsWith) {
    if (isDefined(gramma) && isDefined(value) && isDefined(longestEndsWith)) {

      // Look for an exact match
      var asString = value.toString();
      if (isDefined(gramma.equals) && isDefined(gramma.equals[asString])) {
        return gramma.equals[asString];
      }

      // Look for a category match, prefering longer matches over shorter ones
      if (isDefined(gramma.endsWith)) {
        var max = Math.min(longestEndsWith, asString.length);
        for (var i = max; i > 0; i--) {
          var suffix = asString.slice(-1 * i);
          if (isDefined(gramma.endsWith[suffix])) {
            return gramma.endsWith[suffix];
          }
        }
      }
    }

    return undefined;
  };

  Chooser.prototype.findCategoryMatch = function(value, rules, locale) {
    // When matching with categories, only integers are supported
    value = Math.floor(value);

    var numberMatch = this.findNumberMatchNoRanges(value, rules);
    if (isDefined(numberMatch)) {
      return numberMatch;
    }

    var gramma = grammarRules[locale] || grammarRules['default'];
    var categoryId = this.pickCategory(gramma, value, grammarRules.longestEndsWith);

    if (isDefined(categoryId)) {
      var category = grammarRules.categories[categoryId];
      return Utils.find(rules, function(rule) { return rule.category === category; });
    }

    return undefined;
  };

  Chooser.prototype.findNumberMatchNoRanges = function(value, rules) {
    var rulesWithoutRanges = Utils.map(rules, function(rule) {
      if (isDefined(rule.arg) && rule.comparison === 'gte') {
        return Utils.extend({}, rule, {comparison: 'eq'});
      } else {
        return rule;
      }
    });

    return this.findNumberMatch(value, rulesWithoutRanges);
  };

  Chooser.prototype.findNumberMatch = function(value, rules) {
    var bestMatch;

    for (var i = 0; i < rules.length; i++) {
      var rule = rules[i];
      var comparison = Chooser.COMPARISONS[rule.comparison];

      if (comparison(value, rule.arg) && (!isDefined(bestMatch) || rule.arg > bestMatch.arg)) {
        bestMatch = rule;
      }
    }

    return bestMatch;
  };

  Chooser.prototype.isValidCategory = function(str) {
    return Utils.contains(grammarRules.categories, str);
  };

  Chooser.prototype.format = function(value, rules, locale) {
    var hasCategory = Utils.find(rules, function(rule) { return isDefined(rule.category); });
    var numericValue = Utils.bestEffortNumberConversion(value);

    var match = hasCategory ? this.findCategoryMatch(numericValue, rules, locale) : this.findNumberMatch(numericValue, rules);
    var text = match ? match.text : undefined;

    if (Utils.isFunction(text)) {
      return text();
    } else {
      return text;
    }
  };

  return t8;
}));
/**
 * Client side i18n support. This class knows how to look up static and dynamic i18n keys in JavaScript. Static i18n
 * keys are expected to be in static i18n cache; dynamic keys are expected to be in dynamic i18n cache provided to
 * t8.Resources constructor
 *
 * var i18nStaticCache = {
 *   cache : {
 *     "mynamespace" : {
 *       "hello_world" : "Hello World"
 *     }
 *   }
 * },
 * i18nDynamicCache = {
 *   cache : {}
 * },
 * resources = new t8.Resources(i18nStaticCache, i18nDynamicCache);
 * resources .get("hello_world", "mynamespace");
 *
 * Output: "Hello World"
 */
(function(root, factory) {
  //call factory to create t8 object
  var lib = factory(t8);
  //set up as global variable for browsers, rhino and node
  root['t8'] = lib;
  //set up as export for node
  if (typeof exports !== 'undefined') {
    module.exports = lib;
  }
}(this, function(t8) {
  "use strict";

  var Resources = t8.Resources = function(i18nCacheStatic, i18nCacheDynamic) {
    this.i18nCacheStatic = i18nCacheStatic;
    this.i18nCacheDynamic = i18nCacheDynamic;
  };
  var Utils = t8.Utils;

  var DYNAMIC_KEY_PREFIX = "__i18n__";

  Resources.prototype.get = function(key, namespace, context, callback) {
    Utils.assert(callback, "get called with null callback");
    Utils.assert(key, "get called with null or empty key");
    Utils.assert(namespace, "get called with null or empty namespace");

    var staticString = this.getStaticString(key, namespace);
    if (Utils.isDefined(staticString)) {
      callback(null, staticString);
    } else {
      this.renderDynamicString(key, namespace, context, callback);
    }
  };

  Resources.prototype.getStaticString = function(key, namespace) {
    Utils.assert(key, "getStaticString called with null or empty key");
    Utils.assert(namespace, "getStaticString called with null or empty namespace");

    if (this.i18nCacheStatic && this.i18nCacheStatic.cache && this.i18nCacheStatic.cache[namespace]) {
      return this.i18nCacheStatic.cache[namespace][key];
    }
    return;
  };

  Resources.prototype.renderDynamicString = function(key, namespace, context, callback) {
    Utils.assert(callback, "renderDynamicString called with null callback");
    Utils.assert(key, "renderDynamicString called with null or empty key");
    Utils.assert(namespace, "renderDynamicString called with null or empty namespace");

    var dynamicKeyName = this.dynamicKeyName(key, namespace);
    if (this.i18nCacheDynamic && this.i18nCacheDynamic.cache && this.i18nCacheDynamic.cache[dynamicKeyName]) {
      t8.renderDynamicString(dynamicKeyName, this.i18nCacheDynamic.cache[dynamicKeyName], context, callback);
    } else {
      callback("Could not find static i18n key " + key + " in static i18n cache nor dynamic i18n template " + dynamicKeyName + " in dynamic i18n cache.");
    }
  };

  Resources.prototype.dynamicKeyName = function(key, namespace) {
    Utils.assert(key, "dynamicKeyName called with null or empty key");
    Utils.assert(namespace, "dynamicKeyName called with null or empty namespace");

    return DYNAMIC_KEY_PREFIX + namespace + "__" + key;
  };

  return t8;
}));
(function(root, factory) {
  //call factory to create t8 object
  var lib = factory(t8);
  //set up as global variable for browsers, rhino and node
  root['t8'] = lib;
  //set up as export for node
  if (typeof exports !== 'undefined') {
    module.exports = lib;
  }
}(this, function(t8) {
  'use strict';

  var Rtl = t8.Rtl = function() {};

  /** ISO-LATIN-1 white space */
  var WHITESPACES = " \n\r\t\f\u00A0\u2028\u2029".split("");

  /** Punctuation characters and other characters to exclude */
  var EXCLUDED_CHARACTERS = "~!@#$%^&*()_+`1234567890-={}|[]\\:\";'<>?,./".split("");

  /** Set of characters to exclude when scanning for first character to validate */
  var EXCLUSION_SET = WHITESPACES.concat(EXCLUDED_CHARACTERS);

  /** Hebrew character code for lower limit. Range is 0590–05FF */
  var ARABIC_LOW = '\u0590';

  /** Arabic character code for upper limit. Range is 0600–06FF */
  var ARABIC_HIGH = '\u06FF';

  var Utils = t8.Utils;

  /**
   * Return index of first non-whitespace/punctuation
   * character if its an Arabic character or -1 if first
   * character is not an Arabic character.
   *
   * @param content content to check
   * @param contentLength length of string
   * @return index of first non-whitespace character
   *   if it is an Arabic character, -1 otherwise.
   * @see {@link WHITESPACES}
   * @see {@link EXCLUDED_CHARACTERS}
   */
  var firstRtlCharacter = function(content)
  {
    if(!Utils.isDefined(content)) {
      return -1;
    }

    var i = 0,
        ch = '\0',
        contentLength = content.length;

    var ch;
    for(var i = 0, len = content.length; i < len; i ++) {
      ch = content.charAt(i);
      if(!Utils.contains(EXCLUSION_SET, ch)) {
        break;
      }
    }

    if (i >= len) {
      return -1;
    } else {
      return (ch >= ARABIC_LOW && ch <= ARABIC_HIGH) ? i : -1;
    }
  }

  /**
   * Decode the html content before evaluating the first RTL character
   * htmlDecode function used here is required by security team
   * and is implemented by Roman Shafigullin using RegExp
   *
   * Decode a given string by replacing a well defined set of html entities
   * and all XML save entities into their corresponding character.
   * The following characters are converted:
   * <xmp>
   * &amp;lt;    ->  &lt;
   * &amp;gt;    ->  &gt;
   * &amp;amp;   ->  &amp;
   * &amp;quot;  ->  &quot;
   * &amp;nbsp;  ->  [non-breaking space]
   * &#xXX;      ->  unescape("%" + XX)
   * &#X;        ->  String.fromCharCode(X)
   * &#0; &x#0;  ->  String.fromCharCode(0xfffd) null byte character is not allowed
   * </xmp>
   * @method decodeHTML
   * @param {String} encodedText The string to decode.
   * @return {String} The decoded string.
  */
  var htmlDecode = (function(undefined) {
    var namedEntities = {
      'nbsp': '\u00a0',
      'lt': '<',
      'gt': '>',
      'amp': '&',
      'quot': '"'
    };

    var rEntities = /&(?:(lt|gt|amp|quot|nbsp)|#x([\da-f]{1,4})|#(\d{1,5}));/ig;

    return function( encodedText ) {
      if (encodedText === null || encodedText === undefined) {
        return null;
      }

      return (encodedText + '').replace(rEntities, function(match, named, hex, dec) {
        // if it matched a named entity...
        if (named) {
          // return the mapped character
          return namedEntities[named];
        }
        // if it was a numeric entity...
        else if (hex || dec) {
          // return the character converted from the numeral
          return String.fromCharCode(parseInt(hex || dec, hex ? 16 : 10) || 0xfffd);
        }
        // otherwise...
        return '\ufffd';
      });
    };
  }());

  /**
   * Find the first non-whitespace/punctuation
   * character and determine whether it falls
   * into a character sequence which indicates
   * right-to-left content.
   *
   * @param content content to verify
   * @return true if content is right-to-left,
   *   false otherwise.
   * @see {@link WHITESPACES}
   * @see {@link EXCLUDED_CHARACTERS}
   *
   */

  Rtl.prototype.isRtl = function(content) {
    return firstRtlCharacter(htmlDecode(content)) != -1;
  }

  return t8;
}));

/*! dust-ui-helpers - v1.3.2 Copyright © 2014 LinkedIn Corporation */
(function(root, factory) {
  factory(dust, t8);
}(this, function(dust, t8) {

  var dustVars = {
    i18n: dust.i18n || {cache: {}}
  };

  t8.renderDynamicString = function (dynamicTemplateName, dynamicTemplate, context, callback) {
    dust.render(dynamicTemplateName, context, callback);
  };

  // shared by dirAttr and isRtl
  var rtl = new t8.Rtl();

  //These variables are used by @format helper.
  //Variable `formatters` is a cache of t8's formatter instances. They will be
  //created and stored in cache on demand, when @format helper is called.
  var formatters = {},
    //Variable `formatHelpers` is  cache of helper functions for formatting.
    //They act as a bridge between @format helper input and t8's interface.
    formatHelpers = {
      name: function (format, locale, params) {

        //create a new instance of NameFormatter if it does not exist yet
        if (!formatters.name) {
          if (typeof t8.NameFormatter !== 'undefined') {
            formatters.name = new t8.NameFormatter();
          } else {
            return dust.log('@format helper can not create instance of NameFormatter. t8.NameFormatter is null or undefined', 'ERROR');
          }
        }
        //mapping between helper format names and formats that NameFormatter understands
        var formatMap = {
          'familiar': 'FAMILIAR_NAME',
          'full'    : 'FULL_NAME',
          'list'    : 'LIST_VIEW',
          'micro'   : 'MICROFORMAT'
        },
        requestedFormat = format.split('.'),
        nameFormats = [];

        //convert helper formats into array of formats that NameFormatter can digest
        for(var i = 0; i < requestedFormat.length; i++) {
          //if requested format can be mapped to a format that t8.NameFormatter understands
          //add it to a list of formats we'll pass into NameFormatter.format call
          if (formatMap[requestedFormat[i]]) {
            nameFormats.push(formatMap[requestedFormat[i]]);
          }
        }

        return formatters.name.format({
          firstName: params.firstName,
          lastName: params.lastName,
          maidenName: params.maidenName,
          lastNameWithHighlight: params.lastNameWithHighlight
        }, nameFormats, locale);
      },
      date: function (format, locale, params) {
        if (!formatters.date) {
          if (typeof t8.DateFormatter !== 'undefined') {
            formatters.date = new t8.DateFormatter();
          } else {
            return dust.log('@format helper can not create instance of DateFormatter. t8.DateFormatter is null or undefined', 'ERROR');
          }
        }

        var datePatterns = {
          mdy: {
            long: 'LL',
            medium: 'll',
            short: 'l'
          },
          md: {
            long: 'UU',
            medium: 'uu',
            short: 'u'
          },
          my: {
            long: 'RR',
            medium: 'rr',
            short: 'r'
          },
          time: {
            long: 'LT',
            medium: 'LT',
            short: 'LT'
          },
          iso: 'YYYY-MM-DDTHH:mm:ssZ'
        },
        fmt = format.split('.'),
        pattern = datePatterns;

        //format is expected to have 3 parts:
        // 1) prefix 'date',
        // 2) date parts for example 'mdy', 'my' or 'md'
        // 3) format length 'long', 'medium' or 'short'
        //for example "date.mdy.long", "date.my.short", etc
        var dateParts = fmt.length > 1 ? fmt[1] : '',
            formatLength = fmt.length > 2 ? fmt[2] : '';

        // first check that the initial format exists in the datePatterns object
        if (typeof pattern[dateParts] !== 'undefined') {
          // then check another level in to see if the length of the pattern exists and is valid
          if (typeof pattern[dateParts][formatLength] !== 'undefined') {
            pattern = pattern[dateParts][formatLength];
          // if the pattern had only 1 length, it's either an ISO format or invalid
          } else if(dateParts === 'iso') {
            pattern = pattern[dateParts];
          } else {
            pattern = pattern[dateParts]['long'];
          }
        } else {
          dust.log('@format helper was called with invalid format ' + format + '. Falling back to default date.mdy.long', 'WARN');
          pattern = datePatterns.mdy['long'];
        }

        return formatters.date.format(params.date, locale, pattern, params.useTimeZone);
      },
      currency: function (format, locale,params) {
        if (!formatters.currency) {
          if (typeof t8.CurrencyFormatter !== 'undefined') {
            formatters.currency = new t8.CurrencyFormatter();
          } else {
            return dust.log('@format helper can not create instance of CurrencyFormatter. t8.CurrencyFormatter is null or undefined', 'ERROR');
          }
        }
        return formatters.currency.format(params.amount, params.currency, locale);
      },
      number: function (format, locale,params) {
        if (!formatters.number) {
          if (typeof t8.NumberFormatter !== 'undefined') {
            formatters.number = new t8.NumberFormatter();
          } else {
            return dust.log('@format helper can not create instance of NumberFormatter. t8.NumberFormatter is null or undefined', 'ERROR');
          }
        }
        return formatters.number.format(params.value, locale);
      },
      string: function (format, locale,params, chunk) {
        if (!formatters.truncation) {
          if (typeof t8.TruncationFormatter !== 'undefined') {
            formatters.truncation = new t8.TruncationFormatter();
          } else {
            return dust.log('@format helper can not create instance of stringFormatter. t8.stringFormatter is null or undefined', 'ERROR');
          }
        }
        return formatters.truncation.format(params.value, params.limit);
      }
    };

  //used by @choice helper
  var chooser;

  function getChooserParams(chunk, context, bodies, params) {
    var CHOOSER_TYPE_NUMBER = 'number',
      CHOOSER_TYPE_BOOLEAN = 'boolean',
      CHOOSER_TYPE_STRING = 'string',
      CHOOSER_DEFAULT_BLOCK = 'block',
      type = params.type ? params.type : CHOOSER_TYPE_NUMBER,
      omitParams = ['key', 'type', 'locale'],
      hasCategories = false,
      categories = {},
      key;

    //get categories for chooser rules skipping reserved params for key, locale and type
    for(key in params) {
      if (omitParams.indexOf(key) < 0) {
        hasCategories = true;
        categories[key] = params[key];
      }
    }

    for(key in bodies) {
      if(key !== CHOOSER_DEFAULT_BLOCK) {
        hasCategories = true;
        categories[key] = bodies[key];
      }
    }

    return  {
      key:  dust.helpers.tap(params.key, chunk, context),
      locale: getLocale(params),
      hasCategories: hasCategories,
      categories: categories,
      isBooleanComparison: type === CHOOSER_TYPE_BOOLEAN,
      isStringComparison: type === CHOOSER_TYPE_STRING,
      isNumericComparison: type === CHOOSER_TYPE_NUMBER
    };
  }

  function toChooserArg(str, text, chunk, context) {
    var CHOOSER_NUMERIC_PREFIX = '_',
      CHOOSER_GREATER_THAN_PREFIX = '_gt_',
      lazyText = function() {
        // Only call tap on the actual value the chooser selects
        return dust.helpers.tap(text, chunk, context);
      };

    if (chooser.isValidCategory(str)) {
      return {
        category: str,
        comparison: 'eq',
        text: lazyText
      };
    } else if (str.startsWith(CHOOSER_GREATER_THAN_PREFIX)) {
      return {
        arg: +str.substring(CHOOSER_GREATER_THAN_PREFIX.length),
        comparison: 'gt',
        text: lazyText
      };
    } else if (str.startsWith(CHOOSER_NUMERIC_PREFIX)) {
      return {
        arg: +str.substring(CHOOSER_NUMERIC_PREFIX.length),
        comparison: 'gte',
        text: lazyText
      };
    } else {
      return dust.log('@choice helper called with invalid chooser key: ' + str, 'ERROR');
    }
  }

  function chooseNumeric(chooserParams, chunk, context) {
    if (typeof chooser === 'undefined') {
      if (typeof t8.Chooser !== 'undefined') {
        chooser = new t8.Chooser();
      } else {
        return dust.log('@choice helper could not create an instance of t8.Chooser', 'ERROR');
      }
    }

    var chosen,
      rules = [];

    for (var chooserCategory in chooserParams.categories) {
      rules.push(toChooserArg(chooserCategory, chooserParams.categories[chooserCategory], chunk, context));
    }

    chosen = chooser.format(chooserParams.key, rules, chooserParams.locale);
    if (typeof chosen === 'undefined') {
      // Fallback to the first rule
      chosen = rules[0].text();
    }
    return chosen;
  }

  function chooseString(chooserParams, chunk, context) {
    var chosen,
      CHOOSER_DEFAULT = 'default';

    if( typeof chooserParams.categories[chooserParams.key] !== 'undefined') {
      chosen = dust.helpers.tap(chooserParams.categories[chooserParams.key], chunk, context);
    } else if ( chooserParams.isStringComparison && typeof chooserParams.categories[CHOOSER_DEFAULT] !== 'undefined') {
      //none of the categories matched, use it here if it was provided
      chosen = dust.helpers.tap(chooserParams.categories[CHOOSER_DEFAULT], chunk, context);
    }
    return chosen;
  }

  //end @choice helper functions

  function getLocale(params) {
    //use locale provided as a param
    if (params && params.locale) {
      return params.locale;
    //if no local is provided as param use local from LI.i18n.getLocale
    } else if (typeof LI !== 'undefined' && typeof LI.i18n !== 'undefined' && typeof LI.i18n.getLocale !== 'undefined') {
      return LI.i18n.getLocale().value;
    }
    return 'en_US';
  }

  function applyFilters(value, chunk, context, params) {
    var filters = '',
      //multiproduct is using attribute 'filters' vs network using attribute 'filter'
      rawFilters = params.filter || params.filters,
      ignoreDefaultFilter = params.ignoreDefaultFilter;

    if (typeof rawFilters !== 'undefined') {
      filters = dust.helpers.tap(rawFilters, chunk, context).split('|');
    }

    if (ignoreDefaultFilter) {
      return value;
    } else {
      return dust.filter(value, 'h', filters);
    }
  }

  var helpers = {
    /**
     * Return translated text for the specified key in the specified template.
     * Note: template is optional, if given overrides the name of the current template rendered
     * Example:
     * <p>{@translate key="hello_world"}Hello World{/i18n}</p>
     * <p>{@translate key="hello_world" text="Hello World"/}</p>
     * Output:
     * <p>Hello World</p>
     * <p>{@translate key="close" template="foo/global"/}</p>
     * Output: if the foo/global template existed
     *  <p>Hello World</p>
     * <p>{@translate key="close" hide="true"/}</p>
     * Output: no output because of hide true
     * <p>{@translate key="close" output="json"/}</p>
     * Output: no output because of output json and can be referenced later with {close|s}
     * @param chunk
     * @param context
     * @param bodies
     * @param params
     *      <p>template="foo/global", lookup template cache</p>
     *      <p>hide="true", does not render the i18n in place, stores it in the given template cache</p>
     *      </p>output="json" , stores the value in the current template cache</p>
     */
    'translate': function(chunk, context, bodies, params) {

      if (typeof params === 'undefined' || typeof params.key === 'undefined') {
        return chunk.setError('@translate helper called with null or undefined "key" attribute');
      }

      var hide = params.hide ? dust.helpers.tap(params.hide, chunk, context) : null;
      if (hide === 'true') {
        // do not render
        // this @translate string will be added to i18n cache with given key, but not output here
        // it can be later output with {@translate key='somekey'/}
        return chunk;
      }

      if (typeof t8.Resources !== 'undefined') {
        dustVars.i18n.resources = new t8.Resources(dust.i18n, dust);
      } else {
        return chunk.setError('Can not create an instance of i18n.Resources. i18n.Resources is undefined');
      }

      function outputValue(key, out, myChunk) {

        if (!context.stack.head) {
          context.stack.head = {};
        }

        //output to chunk
        if (params.output === 'json') {
          context.stack.head[key] = out;
          myChunk.end('');
        } else {
          myChunk.end(applyFilters(out, myChunk, context, params));
        }
      }

      function useFallbackValue(key, myChunk) {
        if (typeof bodies !== 'undefined' && typeof bodies.block !== 'undefined') {
          myChunk.capture(bodies.block, context, function(out, captureChunk) {
            outputValue(key, out, myChunk);
            captureChunk.end('');
          }).end();
        } else {
          var output;
           if (typeof params.text !== 'undefined') {
             output = params.text;
           } else {
            output =  key;
           }
          outputValue(key, output, myChunk);
        }
      }

      var key = dust.helpers.tap(params.key, chunk, context),
        template = (typeof context.getTemplateName === 'function') ? context.getTemplateName() : context.global.__template_name__;

      //by default use context.getTemplateName(). It can be overwritten with attributes "templateName" or "template"
      //multiproduct @translate helper is using attribute 'templateName' vs network is using attribute 'template'
      if (params.template) {
        template = dust.helpers.tap(params.template, chunk, context);
      } else if (params.templateName) {
        template = dust.helpers.tap(params.templateName, chunk, context);
      }

      return chunk.map(function(chunk) {
        var i18nContext,
          excludeParams= ['key', 'template'],
          contextOverrides = {};

        //support overriding context with params on @i18n helper
        for (var param in params) {
          if(excludeParams.indexOf(param) < 0) {
            contextOverrides[param] = params[param];
          }
        }
        i18nContext = context.push(contextOverrides);

        if (typeof template !== 'undefined') {
          dustVars.i18n.resources.get(key, template, i18nContext, function(err, out) {
            if (err) {
              dust.log(err);
              useFallbackValue(key, chunk);
            } else {
              outputValue(key, out, chunk);
            }
          });
        } else {
          dust.log('@translate helper can not determine templateName');
          useFallbackValue(key, chunk);
        }
        chunk.end('');
      });
    },

    /**
     * Returns formatted value according to requested format
     * Note: Locale by default will be set to value in LI.i18n.getLocale. You can optionally override the default locale by
     * providing locale param to the helper
     *
     * ============================
     * NAME
     * ============================
     * Example:
     * <p>{@format key="invitee_full_Name" type="name.full" firstName="Kunal" lastName="Cholera" maidenName="Mukesh"/}</p>
     * Output (for en_US locale):
     * <p>Kunal (Mukesh) Cholera</p>
     *
     * <p>{@format key="invitee_full_Name" type="name.full" firstName="Kunal" lastName="Cholera" maidenName="Mukesh" locale="cs_CZ"/}</p>
     * Output (for cs_CZ locale):
     * <p>Kunal Cholera (roz. Mukesh)</p>
     *
     *{@format key="inviter_familiar_Name" type="name.familiar" firstName="Kunal" lastName="Cholera" maidenName="Mukesh" output="json"/}
     *<p>{inviter_familiar_Name|s}</p>
     * Output (for en_US locale):
     * <p>Kunal</p>
     *
     *
     * ============================
     * DATE
     * ============================
     * <p>{@format key="date_started_mdy_long" type="date.mdy.long" date="2012-12-25T06:45:00Z" locale="en_US" useTimeZone=false/}
     * Output (for en_US locale):
     * <p>December 25 2012</p>
     *
     * <p>{@format key="date_started_md_medium" type="date.md.medium" date="2012-12-25T06:45:00Z" locale="en_US" useTimeZone=false/}
     * Output (for en_US locale):
     * <p>Dec 25</p>
     *
     * <p>{@format key="date_started_time_short" type="date.time.short" date="2012-12-25T06:45:00Z" locale="en_US" useTimeZone=false/}
     * Output (for en_US locale):
     * <p>6:45 AM</p>
     *
     *
     *
     *
     * Example:
     * @param chunk
     * @param context
     * @param bodies
     * @param params
     */
    'format': function(chunk, context, bodies, params) {
      if (!params || !params.type) {
        return chunk.setError('@format helper called with null or undefined "format" attribute');
      }

      var type = params.type.split('.')[0],
        output = '',
        format = params.type,
        locale = getLocale(params);

      //get formatted value if that type of formatter exists
      if (typeof formatHelpers[type] !== 'undefined') {
        output = formatHelpers[type](format, locale, params);
      }

      //add the resulting formatted text into context if key is provided
      if (params.key) {
        context.stack.head[params.key] = output;
      }

      //if output is json do not write into output html
      if (params.output && params.output === 'json') {
        return chunk;
      } else {
        if (type === 'name' &&                                        // name that
            (format.indexOf('micro') !== -1 ||                        // is a microformat
            (typeof params.lastNameWithHighlight !== 'undefined'))) { // or has a lastName with html
          params.ignoreDefaultFilter = true;
        }

        return chunk.write(applyFilters(output, chunk, context, params));
      }
    },

    /**
     * Selects the proper singular/plural text to use depending on the value of some number and the rules
     * for the current locale.
     *
     * There are three types of comparison that this helper does: numeric, string and boolean. Numeric comparison is the
     * default type. Type of the comparison can be specified via type attribute. For example type="number", type="string"
     * or type="boolean".
     *
     * There are two patterns for using helper with numeric comparison. The first pattern is to use plural categories:
     *
     * You have {@choice key=count singular="{count} connections" dual="{count} connection" plural="{count} connections"/}.
     *
     * There are rules defined for each language that match a given number to a category like "singular" or "plural". For
     * English rules, if the count was 0, you'd get "0 connections" (English has an "endsWith" rule that classifies 0 as
     * "plural"); if the count was 1, you'd get "1 connection" (English has an "equals" rule that classifies 1 as
     * "singular").
     *
     *
     * The second pattern is to use numbers:
     *
     * You have {@choice key=count _0="{count} connections" _1="{count} connection" _2="{count} connections"/}.
     *
     * The helper above picks the right text to display based on the value of the variable count. It will pick the
     * largest param that is less than or equal to count (use _gt_<num> for strictly less than). In the example above, if
     * count was 0, you'd get "0 connections" (_0 is the closest match); if count was 50, you'd get "50 connections"
     * (_2 is the closest match).
     *
     * Note that if you combine first two patterns, the numbers act as exact matches:
     *
     * You have {@choice key=count _0="no connections" _gt_500="many connections" singular="{count} connections" dual="{count} connection" plural="{count} connections"/}.
     *
     * In the example above, the "no connections" text will *only* show up for a count of zero; everything between 0 and
     * 500 will be handled by the singular/dual/plural rules until you get to > 500, at which point the
     * "many connections" rule takes over.
     *
     *
     * String and boolean type of comparison behave the same. String comparison also supports default text, which boolean
     * does not. For both of these cases the value in the key will be exactly matched to the category name.
     *
     * Your {@choice type="string" key=type job_seeker="Job Seeker" recruiter="Recruiter" default="LinkedIn Premium"/} subscription allows you to reach people out of your network.
     *
     * Here "Job Seeker" will show when type equals "job_seeker" and "Recruiter" will be shown when type equals "recruiter".
     * "LinkedIn Premium" text will be selected when value in key parameter is neither job_seeker no recruiter.
     *
     * Here is an example of boolean comparison:
     *
     *  Your subscription {@choice type="boolean" key=hasInMails true="has" false="does not have"} InMail credits.
     *
     * @param chunk
     * @param context
     * @param bodies
     * @param params
     */
    'choice': function(chunk, context, bodies, params) {
      if (!params || !params.hasOwnProperty('key')) {
        return chunk.setError('@choice helper called without required parameter "key"');
      } else if(typeof(params.key) === 'undefined') {
        dust.log('@choice helper called with undefined key', 'WARN');
        return chunk.write('');
      }

      var chosen = '',
        chooserParams = getChooserParams(chunk, context, bodies, params);

      if (!chooserParams.hasCategories) {
        return chunk.setError('@choice helper called with no patterns to choose from');
      }

      if (chooserParams.isBooleanComparison || chooserParams.isStringComparison) {
        //if key is not a number pick category by exact key match
        chosen = chooseString(chooserParams, chunk, context);
      } else if (chooserParams.isNumericComparison) {
        //if key is a number pick category by range match
        chosen = chooseNumeric(chooserParams, chunk, context);
      }

      return chunk.write(applyFilters(chosen, chunk, context, params));
    },

    /**
     * A helper to render text as possessive for the current locale.
     *
     * Example:
     *
     * See all of {@possessive key=name/} connections.
     *
     * Output:
     *
     * See all of Jim's connections.
     *
     * @param chunk
     * @param context
     * @param bodies
     * @param params
     */
    'possessive': function (chunk, context, bodies, params) {
      if (!params || !params.hasOwnProperty('key')) {
        return chunk.setError('@possessive helper called without required parameter "key"');
      } else if(typeof(params.key) === 'undefined') {
        dust.log('@possessive helper called with undefined key', 'WARN');
        return chunk.write('');
      }

      var key = dust.helpers.tap(params.key, chunk, context),
        possessive = new t8.Possessive(),
        locale = getLocale(params),
        asPossessive = possessive.format(key, locale);

      return chunk.write(applyFilters(asPossessive, chunk, context, params));
    },

    /**
     * A helper to inject the dir attribute. If
     * the given text is considered right-to-left
     * "dir='rtl'" is rendered, otherwise "dir='ltr'"
     * is rendered.
     *
     * Example:
     *
     * <div {@dirAttr text=text}>{text}</div>
     *
     * Output:
     *
     * <div dir='rtl'>the text</div>
     * <div dir='ltr'>the text</div>
     */
    'dirAttr': function (chunk, context, bodies, params) {
      if (!params || !params.hasOwnProperty('text')) {
        return chunk.setError('@dirAttr helper called without required parameter "text"');
      }
      return chunk.write('dir="' + (rtl.isRtl(dust.helpers.tap(params.text, chunk, context))? 'rtl' : 'ltr') + '"');
    },

    /**
     * A helper to conditionally render content.
     * If the given text is considered right-to-left
     * render if block, otherwise render else block.
     * The method will log a message if no body block
     * is provided.
     *
     * Example:
     *
     * {@isRtl text=ugc}yes{:else}no{/isRtl}</div>
     * {@isRtl text=ugc}foo{/isRtl}</div>
     *
     * Output:
     *
     * yes
     * no
     */
    'isRtl': function (chunk, context, bodies, params) {
      var body = bodies.block,
          skip = bodies['else'];

      if (!params || !params.hasOwnProperty('text')) {
        return chunk.setError('@isRtl helper called without required parameter "text"');
      }

      if(rtl.isRtl(dust.helpers.tap(params.text, chunk, context))) {
        if (body) {
          chunk.render(body, context);
        } else {
          dust.log( 'Missing body block in the isRtl helper!', 'INFO' );
        }
      } else {
        if (skip) {
          chunk.render(skip, context);
        }
      }

      return chunk;
    }
  };

  var key;

  for (key in dustVars) {
    dust[key] = dustVars[key];
  }

  for (key in helpers) {
    dust.helpers[key] = helpers[key];
  }
}));


// Monkey patch Dust.log
(function (root, dust) {
  var oldDustLog;
  if (dust.log) {
    oldDustLog = dust.log;
    dust.log = function dustErrorReportToJet(message, type) {
      try {
        if (root.jet && (type === 'ERROR' || type === 'WARN')) {
          if (message instanceof Error) {
            jet.error(message);
          } else if (typeof message === 'string') {
            try {
              throw new Error(message);
            } catch (err) {
              jet.error(err);
            }
          }
        }
      } finally {
        return oldDustLog.apply(dust, arguments);
      }
    };
  } else if (root.jet) {
    jet.error(new Error("The function dust.log doesn't exist in this version."));
  }
}(this, dust));

/**
 * This file is copied from util-i18n-js/src/dust/html-utils.js
 *
 * Utility functions for creating HTML markup
 */
(function(LI, dust, t8, _) {

  "use strict";

  var HtmlUtils = LI.HtmlUtils = {};
  var Utils = t8.Utils;

  /**
   * Convenient wrapper extracts filter options from a params object
   * and uses dust.filter() to filter a string.
   *
   * Example:
   *
   * dustFilter(str, 's|j')
   *
   * Returns:
   *
   * str with HTML unescaped and Javascript escaped
   *
   * @param str string to filter
   * @param params
   * @return {String}
   */
  HtmlUtils.dustFilter = function(str, params) {
    var filters = [];

    if (params && params.filters) {
      filters = _.map(params.filters.split('|'), function(elem) { return elem.trim(); });
    }

    return dust.filter(str, 'h', filters);
  };

  /**
   * Create an HTML tag with the given name, attributes, and body. Properly handles escaping characters for display as
   * HTML. Correctly handles closing tags, including scripts. Note: this function does NOT HTML escape the (optional)
   * body parameter. As the body may contain arbitrary markup, you must ensure that your own code handles escaping for
   * it.
   *
   * Examples:
   *
   * createHtmlTag("div", {id: "foo"}, "bar")
   *
   * Returns: <div id="foo">bar</div>
   *
   * createHtmlTag("script", {src: "foo.js"})
   *
   * Returns: <script src="foo.js"></script>
   *
   * @param tagName
   * @param attributes
   * @param body
   * @return {String}
   */
  HtmlUtils.createHtmlTag = function(tagName, attributes, body) {
    Utils.assert(tagName, "createHtmlTag called with null or undefined tagName");

    var tagNameEscaped = dust.escapeHtml(tagName);
    var out = "<" + tagNameEscaped;

    if (attributes) {
      var htmlAttributes = HtmlUtils.objectToHtmlAttributes(attributes);
      if (htmlAttributes && htmlAttributes.length > 0) {
        out += " " + htmlAttributes;
      }
    }

    var closedTag = "</" + tagNameEscaped + ">";

    if (body) {
      out += ">" + body + closedTag;
    } else if (tagNameEscaped === "script") {
      out += ">" + closedTag;
    } else {
      out += "/>";
    }

    return out;
  };

  /**
   * Converts the key/value pairs in obj to key/value pairs for HTML attributes. Handles escaping correctly.
   *
   * Example:
   *
   * objectToHtmlAttributes({id: "foo", class: "bar"})
   *
   * Returns: id="foo" class="bar"
   *
   * @param obj
   * @return {*}
   */
  HtmlUtils.objectToHtmlAttributes = function(obj) {
    var asHtmlAttrs = _.map(obj, function(value, key) {
      if (Utils.isDefined(key) && Utils.isDefined(value)) {
        return dust.escapeHtml(key) + '="' + dust.escapeHtml(value) + '"';
      } else {
        return null;
      }
    });

    return _.without(asHtmlAttrs, null).join(' ');
  };

})(LI, dust, t8, _);

(function(root) {
  "use strict";
  root.play = root.play || {};
  root.sc = root.sc || {};
  sc.hashes = sc.hashes || {};
})(this);

/**
 * Helpers that extend dust functionality in a non Play/LinkedIn manner. In fact, after some testing in Play apps, if
 * these prove to be generally useful helpers, they should be ported into the dust or dust-helpers open source
 * repositories.
 */
(function(play, dust, t8) {

  "use strict";

  var Utils = t8.Utils;

  /**
   * True iff this script is executing in a browser environment.
   *
   * TODO: USSR should set a flag instead of this hacky check
   *
   * @type {Boolean}
   */
  play.isClient = typeof window !== 'undefined' && typeof document !== 'undefined';

  /**
   * Returns the current dust debug level.
   *
   * @returns {*}
   */
  play.getDustDebugLevel = function() {
    return play.getPageContextValue("dustDebug", false);
  };

  var resourcesInProgress = {};
  /**
   * Support lazy-loading of dust templates and partials. Dust will call dust.onLoad if you try to render a template
   * that is not in dust.cache. This method fetches it asynchronously from play.templateUrl.
   *
   * NOTE: We skip invocation of this when javadust is defined in order to keep the existing dust.onLoad behaviour of throwing an error.
   * This is to prevent errors during asynchronous operations (such as nextTick() calls) being lost when dust.onLoad is overwritten.
   *
   * @param name
   * @param callback
   */
  if (typeof javadust !== undefined) {
    dust.onLoad = function(name, callback) {
      Utils.assert(play.isClient, "Could not find template " + name + ". Lazy loading for dust templates is only available for client-side rendering!");

      var callbacks = resourcesInProgress[name];
      if (callbacks) { // someone is already fetching this template in the background, add myself as a callback
        callbacks.push(callback);
      } else {
        resourcesInProgress[name] = [callback];
        play.getScript(play.templateUrl(name), function() {
          // notify everyone who was waiting on this template that it's ready
          var toNotify = resourcesInProgress[name];
          while (toNotify && toNotify.length > 0) {
            var cb = toNotify.pop();
            cb();
          }
        });
      }
    };
  }

  /**
   * Tap all values from given dust params object. Returns an object with the same key and tapped values.
   *
   * @param params
   * @param chunk
   * @param context
   * @returns {{}}
   */
  dust.helpers.tapAll = function(params, chunk, context) {
    Utils.assert(params, "tapAll called with null params");

    var tappedParams = {};
    _.each(params, function(value, key) {
      tappedParams[key] = dust.helpers.tap(value, chunk, context);
    });

    return tappedParams;
  };

  /**
   * Add a variable to the current context with a key equal to the 'name' parameter and value equal to the body of this
   * tag.
   *
   * Example:
   *
   * {@addToContext name="myVariable"}This text is now available in the context{/addToContext}
   *
   * <p>Now I can use the new variable: {myVariable}</p>
   *
   * Output:
   *
   * <p>Now I can use the new variable: This text is now available in the context</p>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.addToContext = function(chunk, context, bodies, params) {
    Utils.assert(params, "@addToContext called with null params");
    Utils.assert(params.name, "@addToContext called with null params.name");
    params = dust.helpers.tapAll(params, chunk, context);

    return chunk.capture(bodies.block, context, function(out, chunk) {
      context.current()[params.name] = out;
      return chunk.end('');
    });
  };


  /**
   * Writes the body of this tag iff this is the first iteration in a loop. This is the opposite of dust's built in
   * @sep helper.
   *
   * Example:
   *
   * people = ["Jim", "Dean", "Kunal"]
   *
   * {#people}
   *   <li>{.} {@first}is awesome!{/first}</li>
   * {/people}
   *
   * Output:
   *
   * <li>Jim is awesome!</li>
   * <li>Dean</li>
   * <li>Kunal</li>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @return {*}
   */
  dust.helpers.first = function(chunk, context, bodies) {
    if (context.stack.index === 0) {
      return bodies.block(chunk, context);
    } else {
      return chunk.write('');
    }
  };

})(play, dust, t8);


/**
 * Helpers for embedding JSON into HTML so it can be read client-side
 */
(function(play, LI, dust, t8, _) {

  "use strict";

  var Utils = t8.Utils;
  var HtmlUtils = LI.HtmlUtils;

  play.EMBEDDED_CONTEXT_ID = "__pageContext__";

  var ESC_FLAGS = "gi";

  var START_COMMENT = "<!--";
  var END_COMMENT = "-->";

  var HTML_ENTITY = {
    dsh: { escaped: '\\u002d\\u002d', unescaped: '--', escaped_re: '\\\\u002d\\\\u002d' }
  };

  /**
   * This will store Play context data for the current page: CDN URLs, locale to use, CSRF token, etc.
   *
   * This value is lazy loaded in one of two ways:
   *
   * 1. When rendering in USSR, this data will be part of the JSON payload and initialized in a dust base page using the
   *    @initContext helper.
   *
   * 2. When rendering in the browser, this will be read from the DOM.
   *
   * This value is now "private" to dust-utils. To access it, you should always use play.getPageContext() and
   * play.getPageContextValue(key).
   *
   * @type {*}
   */
  play.pageContext = null;

  /**
   * Get the Play context data for the current page, including the CDN URLs, locale to use, CSRF token, etc.
   *
   * @return {*}
   */
  play.getPageContext = function() {
    if (play.pageContext) {
      return play.pageContext;
    } else if (play.isClient) {
      play.setPageContext(play.getEmbeddedContent(play.EMBEDDED_CONTEXT_ID));
      return play.pageContext;
    } else {
      throw "The pageContext is null. Did you call the @initContext helper in the body of your dust base page?";
    }
  };

  /**
   * Set the pageContext to a custom value. In general, this should only be used by dust-utils and its tests.
   *
   * @param context
   */
  play.setPageContext = function(context) {
    Utils.assert(context, "setPageContext called with a null context");
    play.pageContext = context;
  };

  /*
   * Removes the pageContext and cleans up any side-effects of setting it.
   */
  play.removePageContext = function() {
    play.pageContext = null;
  };

  /**
   * Read a value from the Play context data for the current page. If required is set to true, this method will throw
   * an exception if the value is null or undefined.
   *
   * Example:
   *
   * var cdnUrl = play.getPageContextValue("cdnUrl")
   *
   * @param key
   * @param required
   * @return {*}
   */
  play.getPageContextValue = function(key, required) {
    var context = play.getPageContext();
    Utils.assert(context, "pageContext is null");

    var value = context[key];
    if (required) {
      Utils.assertDefined(value, 'The value for ' + key + ' in the pageContext was null or undefined');
    }
    return value;
  };

  /**
   * Escapes characters to their equivalent javascript unicode escape entities within the given string.
   * Does not escape other HTML entities such as the quote, since that's not necessary to make fizzy
   * work at this time. Currently, the only value is '-' which is '\\u002d'.
   *
   * See unescape comment below for more details.
   *
   * Stolen from fz.js
   *
   * @param str
   * @return {*}
   */
  play.escapeForEmbedding = function(str) {
    if (Utils.isDefined(str)) {
      return str.replace(new RegExp(HTML_ENTITY.dsh.unescaped, ESC_FLAGS), HTML_ENTITY.dsh.escaped);
    } else {
      return str;
    }
  };

  /**
   * Unescapes various special characters which fizzy escapes to prevent premature breakout of
   * surrounding tags. Currently unescapes the javascript string unicode escape sequence:
   * '\u002d\u002d' to '--' (dash). This is to protect against the appearance of --, which in some
   * browsers terminates an HTML comment block.
   *
   * This enables us to escape in play in a backwards and forwards compatible way, because old fizzy
   * client.js was expecting &dsh; (which won't appear anymore, so unescaping won't have any impact),
   * and the '\u002d\u002d' sequence, since it is only valid within a string for JSON, will naturally
   * unescape into '--'.
   *
   * Browsers that terminate comments on a single - are vulnerable.
   *
   * Stolen from fz.js
   *
   * @param str
   * @return {*}
   */
  play.unescapeForEmbedding = function (str) {
    if (Utils.isDefined(str)) {
      return str.replace(new RegExp(HTML_ENTITY.dsh.escaped_re, ESC_FLAGS), HTML_ENTITY.dsh.unescaped);
    } else {
      return str;
    }
  };

  /**
   * Creates an HTML tag with the given JSON embedded in a format that can be read safely on the client-side
   *
   * Example:
   *
   * embeddedJsonTag({foo: "bar"}, "my-tag")
   *
   * Output:
   *
   * <code id="my-tag" style="display: none;"><!--{"foo": "bar"}--></code>
   *
   * @param json
   * @param id
   * @return {String}
   */
  play.embeddedJsonTag = function(json, id) {
    Utils.assert(id, "embeddedJsonTag called with null id");

    var attrs = {id: id, style: "display: none;"};
    var body = play.wrapInComment(JSON.stringify(json));
    return HtmlUtils.createHtmlTag("code", attrs, body);
  };

  /**
   * Wrap a piece of text in HTML comments
   *
   * @param text
   * @return {String}
   */
  play.wrapInComment = function(text) {
    Utils.assertDefined(text, "wrapInComment called with null text");

    return START_COMMENT + play.escapeForEmbedding(text) + END_COMMENT;
  };

  /**
   * Read JSON content embedded in an HTML <code> tag. The play.embeddedJsonTag function and the @embedJSON helper can
   * safely store JSON in the body of an HTML document. This method can safely read it back out. This is useful for
   * exposing JSON assembled server-side to client-side JavaScript. Note that this method does not do any caching;
   * since JSON.parse can be expensive, you should locally cache the data yourself (e.g. in a backbone model) if you're
   * going to be accessing this data more than once for an id.
   *
   * This code is largely copied from fz.js: I'd prefer to use fs.payload, but it only works of fs.embed was called
   * previously, which does not apply to all embedded JSON use cases.
   *
   * @param id
   */
  play.getEmbeddedContent = function(id) {
    Utils.assert(id, "getEmbeddedContent called with null id");

    var contentElem = document.getElementById(id);
    Utils.assert(contentElem, "Could not find DOM node with id " + id);
    Utils.assert(contentElem.firstChild, "DOM node with id " + id + " did not have a child comment node");

    var innerContent = contentElem.firstChild.nodeValue;
    Utils.assert(innerContent !== null && innerContent.length > 0, "No inner contents found for DOM node with id " + id);

    contentElem.parentNode.removeChild(contentElem);
    return JSON.parse(play.unescapeForEmbedding(innerContent));
  };

  /**
   * Include this in a base page to setup client-side rendering of a template. This is useful if your pages are simple,
   * or you already fetched all the JSON you need for the base page, or you don't want to incur an extra roundtrip from
   * Fizzy and setting up an extra endpoint for an embed.
   *
   * Supported parameters:
   *
   * 1. template: the template to render. Should be the name the template registers in dust.cache. Required.
   *
   * 2. data: the JSON payload that will be passed into the template during rendering. It will be embedded in the
   *          HTML as a <code> tag. Optional.
   *
   * 3. templateId: the id to use for the template script tag and the embedded JSON payload (the latter will have
   *                -content appended). Optional. Uses the template name if not provided. If you're calling @render
   *                multiple times for the same payload, make sure to give each one a unique id.
   *
   * 4. templateUrl: the URL for the template. Optional. Tries to guess URL using play.templateUrl if not provided.
   *
   * 5. skipTemplateUrl: if set to true, does not include a script tag to fetch the template, with the assumption that
   *                     the template is _already_ in dust.cache. Optional. Default is false.
   *
   * 6. containerId: the ID of the DOM element which the rendered template will be inserted into. Note, this DOM node
   *                 must already be in the DOM; during initial page load, this usually means the DOM comes before this
   *                 @render call. Optional. If no ID is given then the embed will be inserted just before the DOM
   *                 element with id templateId.
   *
   * Example:
   *
   * {@render template="/templates/home" data=contextData /}
   *
   * Output:
   *
   * <script src="/assets/templates/home.js"></script>
   * <code id="my-tag" style="display: none;"><!--{"foo": "bar"}--></code>
   * <script>fs.embed("templates/home", "templates/home");</script>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.render = function(chunk, context, bodies, params) {
    Utils.assert(!play.isClient, "The @render helper is only used in a server-side rendered base page to setup client-side rendering. Perhaps you want fs.embed() instead?");
    Utils.assert(params, "@render called with null params");
    Utils.assert(params.template, "@render called with null params.template");
    params = dust.helpers.tapAll(params, chunk, context);

    var template = params.template;
    var data = params.data || {};
    var templateId = params.templateId || template;
    var templateUrl = params.templateUrl || play.templateUrl(template);
    var skipTemplateUrl = params.skipTemplateUrl;
    var containerId = params.containerId;

    var scriptTagParams = {src: templateUrl, id: templateId};
    var fetchTemplateScriptTag = skipTemplateUrl ? "" : HtmlUtils.createHtmlTag("script", scriptTagParams);

    var embeddedJsonTag = play.embeddedJsonTag(data, templateId + "-content");

    var asJsString = function(str) {
      if (Utils.isDefined(str)) {
        return '"' + dust.escapeJs(str) + '"';
      } else {
        return "undefined";
      }
    };

    var fsEmbedParams = _.map([templateId, template, undefined, containerId], asJsString);

    var fsEmbedBody = 'fs.embed(' + fsEmbedParams.join(', ') + ');';
    var fsEmbedTag = HtmlUtils.createHtmlTag("script", {}, fsEmbedBody);

    return chunk.write(fetchTemplateScriptTag + embeddedJsonTag + fsEmbedTag);
  };

  /**
   * Embed the JSON specified in by the data parameter in a code tag with the id in the id parameter. This can be used
   * to take JSON assembled server-side and safely embed it in the HTML document. Use play.getEmbeddedContent to read
   * it back out later.
   *
   * Example: {@embedJSON id="my-data" data=someJsonInContext /}
   *
   * Output: <code id="my-data" style="display: none"><!--{"foo":"bar"}--></code>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @returns {*}
   */
  dust.helpers.embedJSON = function(chunk, context, bodies, params) {
    Utils.assert(params, "@embedJSON called with null params");
    Utils.assert(params.id, "@embedJSON called with null id");
    Utils.assert(params.data, "@embedJSON called with null data");
    params = dust.helpers.tapAll(params, chunk, context);

    return chunk.write(play.embeddedJsonTag(params.data, params.id));
  };

  /**
   * Use this helper in your base pages to setup the page context for client-side rendering. Most helpers in dust-utils
   * will not work if this helper is not included.
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.initContext = function(chunk) {
    Utils.assert(!play.isClient, "The @initContext helper is only used in a server-side rendered base page to setup the page context for server and client-side rendering.");

    // Embed the page context JSON in the DOM so it is available to dust-utils for client-side rendering
    var embeddedJsonCodeTag = play.embeddedJsonTag(play.getPageContext(), play.EMBEDDED_CONTEXT_ID);

    var dustDebugLevel = dust.escapeHtml(dust.escapeJs(play.getDustDebugLevel()));
    var dustDebugScriptTag = HtmlUtils.createHtmlTag("script", {}, 'if (dust) {dust.debugLevel = "' + dustDebugLevel + '";}');

    // Inject sc-hashes_xx_XX.js if useScHashesJs
    // TODO: Find a way to inject dust-utils.js and sc-hashes_xx_XX.js in a more performant manner (e.g. let users customize in their concat group)
    var scHashesScriptTag = play.useScHashesJs() ? HtmlUtils.createHtmlTag("script", {src: play.getPageContextValue("scHashesUrl", true)}) : '';

    // Inject dust-utils.js
    var dustUtilScriptTag = HtmlUtils.createHtmlTag("script", {src: play.getPageContextValue("dustUtilsUrl", true)});

    // When JavaScript reverse routers are included in DustOptions, a URL to fetch them is included in the response to
    // Fizzy/USSR. The URL will return JavaScript with the JavaScript reverse routers so they are usable during server
    // side rendering. It also returns a play.jsRoutesString that we put into a script tag so the same routes are
    // available for client side rendering.
    var jsRoutesScriptTag = play.jsRoutesString ? HtmlUtils.createHtmlTag("script", {}, play.jsRoutesString.replace(/__NEW_LINE__/g, "\n")) : '';

    return chunk.write(embeddedJsonCodeTag + dustDebugScriptTag + scHashesScriptTag + dustUtilScriptTag + jsRoutesScriptTag);
  };

  // When rendering server-side, hijack (monkey patch) the dust.render method so we can access the page context
  if (!play.isClient && !play.contextReady) {

    // Prevent the context from being initialized multiple times
    play.contextReady = true;

    var originalRenderFunction = dust.render;

    dust.render = function(name, context, callback) {
      var pageContext = context[play.EMBEDDED_CONTEXT_ID];
      Utils.assert(pageContext, "No page context found!");

      play.setPageContext(pageContext);

      dust.render = originalRenderFunction;
      originalRenderFunction(name, context, callback);
    };
  }

})(play, LI, dust, t8, _);

/**
 * Helpers for rendering forms in dust. Experimental.
 */
(function(play, LI, dust, t8, _) {

  "use strict";

  var Utils = t8.Utils;
  var HtmlUtils = LI.HtmlUtils;

  /**
   * Writes this tag's body surrounded by form tags. Includes a hidden CSRF input in the form. You can specify an
   * 'alias' parameter and corresponding arg0, arg1, etc. parameters to fill in the form's action attribute with a URL.
   * You can also specify a 'formData' parameter to provide "form data" for the @input helper.
   *
   * Any parameter other than 'alias' and 'formData' will be passed unchanged to the form tag.
   *
   * Example:
   *
   * {@form alias="controllers.Forms.submit" id="myForm"}
   *   <input type="text" name="name"/>
   *   <input type="submit"/>
   * {/form}
   *
   * Output:
   *
   * <form action="/dust-sample/submit" method="post" id="myForm">
   *   <input type="hidden" name="csrfToken" value="ajax:12345"/>
   *   <input type="text" name="name"/>
   *   <input type="submit"/>
   * </form>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.form = function(chunk, context, bodies, params) {
    params = params || {};
    params = dust.helpers.tapAll(params, chunk, context);

    if (params.formData) {
      var formData = this.tap(params.formData, chunk, context);
      context = context.push({formData: formData});
    }

    var attrs = {method: "POST"};
    var body = play.createCsrfInput();

    if (params.alias) {
      attrs.action = play.url(params);
    }

    attrs = _.extend({}, attrs, _.omit(params, ['alias', 'formData']));

    if (bodies && bodies.block) {
      return chunk.capture(bodies.block, context, function(out, chunk) {
        body += out;
        return chunk.end(HtmlUtils.createHtmlTag("form", attrs, body));
      });
    } else {
      return chunk.write(HtmlUtils.createHtmlTag("form", attrs, body));
    }
  };

  // TODO: handle enums as drop downs or radio buttons
  // TODO: handle booleans as check boxes
  /**
   * Write a form input based on JSON form data produced by PegasusFormPlugin.recordTemplateAsFormJson. Specify the
   * field to render via the 'field' param and the form data for the fields via the `formData` param. Alternatively, if
   * this is inside of a @form tag, you can add the `formData` to the @form tag and it'll apply to all @input calls
   * inside of it.
   *
   * The input will use the field's id as its id and name attributes. It will also include a label and a div for
   * displaying errors.
   *
   * You can customize behavior using the following parameters:
   *
   * 1. label: custom label text (default: field name)
   * 2. noLabel: don't include a label
   * 3. error: error text to display (default: none)
   * 4. noError: don't include an error div
   * 5. arrayIndex: will append [arrayIndex] to the id and name of the field. Useful for submitting lists.
   *
   * Examples:
   *
   * {@input field="name" formData="myFormData"/}
   *
   * {@form alias="controllers.Forms.submit" formData="myFormData"}
   *   {@input field="name"/}
   *   {@input field="age" noLabel="true" noError="true" id="custom-id"/}
   *   {@input field="preferences" type="checkbox" arrayIndex="0" noLabel="true" noError="true"/}
   *   {@input field="preferences" type="checkbox" arrayIndex="1" noLabel="true" noError="true"/}
   * {/form}
   *
   * Output:
   *
   * <label for="name">name</label>
   * <div class="error" id="name-error"></div>
   * <input type="text" id="name" name="name"/>
   *
   * <form action="/dust-sample/submit" method="post">
   *   <input type="hidden" name="csrfToken" value="ajax:12345"/>
   *
   *   <label for="name">name</label>
   *   <div class="error" id="name-error"></div>
   *   <input type="text" id="name" name="name"/>
   *
   *   <input type="text" id="custom-id" name="age"/>
   *
   *   <input type="checkbox" id="preferences[0]" name="preferences[0]"/>
   *   <input type="checkbox" id="preferences[0]" name="preferences[0]"/>
   * </form>
   *
   * {@input fiel
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.input = function(chunk, context, bodies, params) {
    Utils.assert(params, "@input called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    var formData = context.get("formData") || params.formData;
    Utils.assert(formData, "@input called with null formData");

    var field = play.getFormField(formData, params.field);
    Utils.assert(field, "@input did not find field " + params.field + " in the formData");
    Utils.assert(field.id, "@input did not find an id in field " + params.field);
    Utils.assert(field.name, "@input did not find a name in field " + params.field);

    var id = field.id;

    if (params.arrayIndex) {
      id += "[" + params.arrayIndex + "]";
    }

    var defaultAttrs = {type: "text", id: id, name: id};

    var value = field.value || field['default'];
    if (value) {
      defaultAttrs.value = value;
    }

    var attrs = _.extend({}, defaultAttrs, _.omit(params, ['field', 'label', 'noLabel', 'noError', 'error', 'formData', 'field', 'arrayIndex']));

    var out = '';

    if (!params.noLabel) {
      var label = params.label || field.name;
      var labelAttrs = {"for": attrs.id};
      if (!field.optional) {
        labelAttrs['class'] = 'required';
      }
      out += HtmlUtils.createHtmlTag("label", labelAttrs, label);
    }

    if (!params.noError) {
      var error = params.error || field.error || '';
      var divAttrs = {
        id: attrs.id + "-error",
        "class": "error"
      };
      out += HtmlUtils.createHtmlTag("div", divAttrs, error);
    }

    out += HtmlUtils.createHtmlTag("input", attrs);

    return chunk.write(out);
  };

  /**
   * Traverses a pegasus form object to find a field. The fieldName will be of the format "A.B.C".
   *
   * @param formData
   * @param fieldName
   * @return {*}
   */
  play.getFormField = function(formData, fieldName) {
    Utils.assert(formData, "getFormField called with null formData");
    Utils.assert(fieldName, "getFormField called with null fieldName");

    var field = formData;

    var parts = fieldName.split(".");
    for (var i = 0; i < parts.length; i++) {
      field = field.fields[parts[i]];
      Utils.assert(field, "Could not find field " + fieldName + " in form data");
    }

    return field;
  };

})(play, LI, dust, t8, _);

/**
 * Helpers for programmatically generating HTML markup. In most cases, you should use a dust template to render markup,
 * but the functions in this file are more convenient for rendering markup from a helper, where the ratio of logic to
 * markup is very high.
 */
(function(play, LI, t8, _) {

  "use strict";

  var Utils = t8.Utils;
  var HtmlUtils = LI.HtmlUtils;

  /**
   * Create a <script> tag with attributes obtained by merging customAttrs and params.
   *
   * @param customAttrs
   * @param params
   * @return {*}
   */
  play.createScriptTag = function(customAttrs, params) {
    Utils.assert(customAttrs, "createScriptTag called with null customAttrs");
    Utils.assert(params, "createScriptTag called with null params");

    var attrs = _.extend({type: "text/javascript"}, _.extend({}, customAttrs, _.omit(params, ['path', 'paths'])));
    return HtmlUtils.createHtmlTag("script", attrs);
  };

  /**
   * Create a <link> tag to load CSS files
   *
   * @param customAttrs
   * @param params
   * @return {*}
   */
  play.createCssTag = function(customAttrs, params) {
    Utils.assert(customAttrs, "createCssTag called with null customAttrs");
    Utils.assert(params, "createCssTag called with null params");

    var attrs = _.extend({rel: 'stylesheet'}, _.extend({}, customAttrs, _.omit(params, ['path', 'paths'])));
    return HtmlUtils.createHtmlTag("link", attrs);
  };

})(play, LI, t8, _);

/**
 * Helpers for dealing with i18n and text formatting in JavaScript.
 */
(function(play, dust, t8, LI) {

  "use strict";

  var Utils = t8.Utils;

  var DEFAULT_LOCALE = "en_US";

  /**
   * The @i18n helper is replaced by @translate in t8. To remain backward compatibility, make @i18n an alias of @translate
   */
  dust.helpers.i18n = dust.helpers.translate;

  /**
   * Keep original LI.i18n.getLocale function to remain backward compatible
   */
  var originalLIi18nGetLocale = LI.i18n.getLocale || function() { return {value: DEFAULT_LOCALE }; };

  /**
   * Hijack LI.i18n.getLocale to get the current locale directly from Play's page context. So that we don't need to
   * explicitly set it.
   *
   * t8 uses this method to determine locale if it's not specified in the helper.
   * Falls back to en_US if not found.
   *
   * If there is no page context defined (used in a non-play app), it will fall back to the original LI.i18n.getLocale function
   *
   * @returns {*|string}
   */
  LI.i18n.getLocale = function() {
    try {
      return {value: play.getPageContextValue("locale", false) || DEFAULT_LOCALE };
    } catch (err) {
      return originalLIi18nGetLocale();
    }
  };

  /**
   * Returns a truncated version of the given input string. Refer to the examples below for
   * truncation behavior.
   *
   * IMPORTANT: Do not use this for strings containing markup
   * IMPORTANT: Only use this for English
   *
   * Examples:
   *
   * {@truncate value="foo bar" length="3"/}
   * {@truncate value="foo bar" length="4"/}
   * {@truncate value="foo bar" length="5"/}
   * {@truncate value="foo bar" length="7"/}
   * {@truncate value="Hoboken, NJ" length="8" /}
   *
   * Output:
   *
   * "foo…"     // straight forward truncate
   * "foo…"     // truncate on whitespace
   * "foo…"     // truncate but don't include partial words
   * "foo bar"  // no truncation needed
   * "Hoboken…" // remove trailing punctuation mark after truncation
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.truncate = function(chunk, context, bodies, params) {
    Utils.assert(params.value, "@truncate called with null value param");
    Utils.assert(params.length, "@truncate called with null length param");
    Utils.assert(params.length > 0, "@truncate called with length param less than one");
    params = dust.helpers.tapAll(params, chunk, context);

    // removes punctuation from the end of a string (and removes trailing+leading whitespace)
    function removeTrailingPunctuation(value) {
      var lastChar = value.charAt(value.length - 1);

      if(lastChar === '.' || lastChar === ',') {
        value = value.substring(0, value.length - 1);
      }
      return value.trim();
    }

    var value = params.value.trim(),
        indexOfLastSpace = 0;

    if(value.length > params.length) {
      var lastChar = value.charAt(params.length - 1);

      if(lastChar === ' ' || lastChar === '\r' || lastChar === '\n' || lastChar === '\t') {
        // if the string is truncated at a white space character, just truncate from there
        value = removeTrailingPunctuation(value.substring(0, params.length)) + '\u2026';
      } else {
        // otherwise truncate from last space to prevent truncation in a word
        value = value.substring(0, params.length);
        indexOfLastSpace = value.lastIndexOf(' ');
        if(indexOfLastSpace > 0) {
          value = removeTrailingPunctuation(value.substring(0, indexOfLastSpace)) + '\u2026';
        } else {
          // allow word truncation when no other option
          value = removeTrailingPunctuation(value) + '\u2026';
        }
      }
    }

    return chunk.write(dust.escapeHtml(value));
  };

})(play, dust, t8, LI);

/**
 * Security related helpers, mostly for dealing with CSRF.
 */
(function(play, LI, dust, t8) {

  "use strict";

  var Utils = t8.Utils;
  var HtmlUtils = LI.HtmlUtils;

  /**
   * Create a hidden <input> element for the CSRF token
   *
   * @return {*}
   */
  play.createCsrfInput = function() {
    var attributes = {
      type: "hidden",
      name: "csrfToken",
      value: play.getPageContextValue("csrfToken", true)
    };
    return HtmlUtils.createHtmlTag("input", attributes, null);
  };

  /**
   * Add the CSRF token to the given URL
   *
   * @param url
   * @return {*}
   */
  play.addCsrfTokenToUrl = function(url) {
    Utils.assert(url, "addCsrfTokenToUrl called with null url");

    return play.addQueryParameter(url, "csrfToken", play.getPageContextValue("csrfToken", true));
  };

  /**
   * Write the CSRF token.
   *
   * Example:
   *
   * {@csrf/}
   *
   * Output:
   *
   * ajax:12345
   *
   * @param chunk
   * @return {*}
   */
  dust.helpers.csrf = function(chunk) {
    return chunk.write(dust.escapeHtml(play.getPageContextValue("csrfToken", true)));
  };

  /**
   * Create a hidden input for a CSRF token.
   *
   * Example:
   *
   * {@createCsrfInput/}
   *
   * Output:
   *
   * <input type="hidden" name="csrfToken" value="ajax:12345"/>
   *
   * @param chunk
   * @return {*}
   */
  dust.helpers.createCsrfInput = function(chunk) {
    return chunk.write(play.createCsrfInput());
  };

  /**
   * Helper to check if current member is a csUser
   *
   * Example:
   *
   * {@isCsUser}
   *   fizzy embeds here
   * {/isCsUser}
   *
   * Output:
   *
   * boolean
   *
   * @param chunk
   * @return {*}
   */
  dust.helpers.isCsUser = function(chunk, context, bodies) {
    if (play.getPageContextValue("isCsUser", false) && bodies.block) {
      return chunk.render(bodies.block, context)
    }
    return chunk;
  };

})(play, LI, dust, t8);

(function(play, dust) {

  "use strict";

  /**
   * Convenience method to get the appName value from the pageContext.
   *
   * TODO BL I'm not (yet) sure whether it was intended to not expose the page context to helpers. If my guess is wrong,
   * then perhaps we can create a more generalised helper to access an arbitrary page context value.
   */
  dust.helpers.contextPath = function(chunk) {
    return chunk.write(play.getPageContextValue("contextPath"));
  }

})(play, dust);

/**
 * Helpers for generating URLs in JavaScript and dust templates.
 */
(function(play, LI, dust, t8, sc, _) {

  "use strict";

  var Utils = t8.Utils;
  var HtmlUtils = LI.HtmlUtils;
  var URL_MAX_LENGTH = 1024;
  var URL_HASH_LENGTH = 25;
  var TYPICAL_CDN_URL_LENGTH = 50;

  play.MEDIA_URN_PREFIX = "urn:li:media:";

  /**
   * If value is a single value, wrap it in an array. If it is already an array, return it unchanged.
   *
   * @param value
   * @returns {*}
   */
  var asArray = function(value) {
    return _.isArray(value) ? value : [value];
  };

  /**
   * Estimates the length of generated url in prod. Will throw exception in dev mode if either the generated versioned or hashed
   * url may exceed the length limit.
   *
   * TODO: CORP apps don't use spark URLs, so we need to a) not do this check and b) prevent them from using dynamic concat in general.
   * @param paths
   */
  var checkUrlLength = function(paths) {
    Utils.assert(paths, "checkUrlLength called with null paths");
    var sparkBaseForFiles = play.getPageContextValue("baseSparkUrlForFiles", true);
    var sparkBaseForHashes = play.getPageContextValue("baseSparkUrlForHashes", true);
    var appName = play.getPageContextValue("appName", true);

    var versionedUrlLength = formatPathsForSpark(appName, paths).length + sparkBaseForFiles.length;
    // +1 for the comma
    var estHashedUrlLength = paths.length * (URL_HASH_LENGTH + 1) + sparkBaseForHashes.length;

    if (_.min([versionedUrlLength, estHashedUrlLength]) > URL_MAX_LENGTH - TYPICAL_CDN_URL_LENGTH) {
      var errorMsg = "Error: The generated URL for paths " + paths + " MAY exceed the max length of " +
                      URL_MAX_LENGTH + " in production. Please break up the URLs into multiple pieces, or use build time concat.";
      if (!play.isProd()) {
        play.log(errorMsg);
      }
    }
  };

  /**
   * Converts the given paths into a format Spark (and the Play assets controller) can understand.
   *
   * See: https://iwww.corp.linkedin.com/wiki/cf/display/ENGS/Spark+Server#SparkServer-ContentServingEndpoints
   *
   * In particular, we:
   *
   * 1. Prepend the app name to each path. This is because we package all static content for a multiproduct into one
   *    artifact and use the app name as a namespace.
   *
   * 2. URL encode each path
   *
   * 3. Join them all with commas
   *
   * @param paths
   * @returns {*}
   */
  var formatPathsForSpark = function(appName, paths) {
    Utils.assert(paths, "formatPathsForSpark called with null paths");

    return _.map(paths, function(path) {
      return encodeURIComponent(play.buildPath("/", appName, "/", path));
    }).join(",");
  };

  /**
   * Builds a versioned spark url from given paths in this form:
   *
   * http://<cdn-host>/sc/p/<mp-org>:<mp-name>-static-content+<app-version>/f/<paths>
   *
   * @param paths
   * @returns {}
   */
  var versionedSparkUrl = function(appName, paths) {
    Utils.assert(paths, "versionedSparkUrl called with null paths");
    paths = formatPathsForSpark(appName, paths);

    var sparkBasePath = play.getPageContextValue("baseSparkUrlForFiles", true);
    return play.appendCdnUrlIfNeeded(play.combineUrlPieces(sparkBasePath, paths));
  };

  /**
   * Builds a hashed spark url from given paths in this form:
   *
   * http://<cdn-host>/sc/h/<hashes>
   *
   * @param paths
   * @returns {}
   */
  var hashedSparkUrl = function(appName, paths) {
    Utils.assert(paths, "hashedSparkUrl called with null paths");
    paths = _.map(paths, function(path) {return sc.hashes[appName][path];}).join(",");

    var sparkBasePath = play.getPageContextValue("baseSparkUrlForHashes", true);
    return play.appendCdnUrlIfNeeded(play.combineUrlPieces(sparkBasePath, paths));
  };

  /**
   * Returns true iff a CDN should be used when building URLs to static assets
   *
   * @return
   */
  play.useCdn = function() {
    return play.getPageContextValue("useCdn", false);
  };

  /**
   * Returns true iff this Play app is running in production mode
   *
   * @return
   */
  play.isProd = function() {
    return play.getPageContextValue("isProd", false);
  };

  /**
   * Returns true iff using sc-hashes.js file on this page
   *
   * @return
   */
  play.useScHashesJs = function() {
    return play.getPageContextValue("useScHashesJs", false);
  };

  /**
   * Returns true iff hashed URL is disabled by uh=f query param
   *
   * @return
   */
  play.hashesDisabledByQueryParam = function() {
    return play.getPageContextValue("hashesDisabledByQueryParam", false);
  };

  /**
   * Add the given key value pair as a query string parameter to the given url. If the key value pair is already in the
   * URL, this will replace the old value.
   *
   * @param url
   * @param key
   * @param value
   * @return {*}
   */
  play.addQueryParameter = function(url, key, value) {
    Utils.assert(url, "addQueryParameter called with null url");
    Utils.assert(key, "addQueryParameter called with null key");
    Utils.assert(value, "addQueryParameter called with null value");

    key = encodeURIComponent(key);
    value = encodeURIComponent(value);

    var re = new RegExp("([?|&])" + key + "=.*?(&|$)", "i");
    if (url.match(re)) {
      return url.replace(re, '$1' + key + "=" + value + '$2');
    } else {
      var questionIndex = url.indexOf('?');
      var separator = '&';
      if (questionIndex < 0) {
        separator = '?';
      } else if (questionIndex === url.length - 1) {
        separator = '';
      }

      return url + separator + key + "=" + value;
    }
  };

  /**
   * Use this helper to get all the parameters as a json object from an url.
   * It accepts a full url like "http://www.linkedin.com?par1=val1&par2=val2"
   * or only the queryString like "?par1=val1&par2=val2".
   * In both cases it returns {par1: ["val1"], par2: ["val2"]} as a js object.
   * Special characters are also welcome, they will be decoded properly.
   * All values for each parameter are returned as arrays to keep type consistence with multiple value parameters.
   * For instance:
   *
   * parseQueryString("par1=val1a&par1=val1b&par2=val2")
   * returns:
   * {par1: ["val1a", "val1b"], par2: ["val2"]}
   *
   * It throws an exception if url is undefined, null, empty or there are more than one "?" character.
   *
   * @param url
   * @return {*}
   */
  play.parseQueryString = function(url) {
    Utils.assertDefined(url, "parseQueryString called with null url");

    var urlParts = url.split("?");
    Utils.assert(urlParts.length <=2 , "Malformed url");

    if (urlParts.length<2) {
      return {};
    }

    var queryString = urlParts[1];  //urlParts.length == 2, so takes the queryString to parse

    // loops and split all parameters, allowing array of parameters to be stored as a js array
    var result = {};
    var keyValues = queryString.split("&");
    _.each(keyValues, function(keyValue) {
      var keyValueParts = keyValue.split("=");
      var key = decodeURIComponent(keyValueParts[0]);
      //discards consecutive ampersands since keys cannot be empty, only values
      if (key) {
        // ternary operator handles parameters equaling to no value like "param1=&param2=value2"
        var value = (keyValueParts.length > 1) ? decodeURIComponent(keyValueParts[1]) : "";
        //creates an array of values even if it's just one
        if (result[key]) {
          result[key].push(value);
        }
        else {
          result[key] = [value];
        }
      }
    });

    return result;
  };

  /**
   * This is a wrapper for parseQueryString that returns only the first element for each key.
   * It's comfortable to use when all parameters in the url have single values, since it eliminates the need
   * to dereference the array.
   *
   * Example:
   * parseQueryString("http://www.linkedin.com?par1=val1&par2=val2") returns {par1: ["val1"], par2: ["val2"]}
   * parseQueryStringSimple("http://www.linkedin.com?par1=val1&par2=val2") returns {par1: "val1", par2: "val2"}
   */
  play.parseQueryStringSimple = function(url) {
    var queryString = play.parseQueryString(url);
    var queryStringSimple = {};

    _.each(queryString, function(value, key) {
      queryStringSimple[key] = value[0];
    });

    return queryStringSimple;
  };

  /**
   * Use this helper to build an url with parameters from a map.
   * Since it allows parameters to be arrays, each key must map to an array of values, even if it is just one (see examples below).
   * Url maybe null or empty so to build only the query string.
   * The initial url may contain additional parameters which will be replaced by values in the  map when names match.
   *
   * Examples:
   *
   * buildUrl("http://www.linkedin.com?par1=val1", {par1: ["val1new"], par2: ["val2new"]})
   * returns:
   * "http://www.linkedin.com?par1=val1new&par2=val2new"
   *
   * buildUrl("", {par1: ["val1new"],par2: ["val2new"]})
   * returns:
   * "par1=val1new&par2=val2new"
   *
   * Examples to build urls with array parameters:
   *
   * buildUrl("http://www.linkedin.com?par1=val1&par2=val2", {par1: ["val1new"], par2: ["val2anew", "val2bnew"]})
   * returns:
   * "http://www.linkedin.com?par1=val1new&par2=val2anew&par2=val2bnew"
   *
   * It throws an exception if urlParameters is undefined or null.
   *
   * @param url
   * @param urlParameters
   * @return {*}
   */
  play.buildUrl = function(url, urlParameters) {
    url = url || "";
    urlParameters = urlParameters || {};

    // takes care of nulls and undefined for url
    var baseUrl = url.split("?")[0];
    var parameters = play.parseQueryString(url);

    //if an existing parameter is also in the urlParameters map, its value is replaced by the new one
    _.each(urlParameters, function(value, key) {
      parameters[key] = value;
    });

    var keyValues = [];
    _.each(parameters, function(values, key) {
      Utils.assert(Array.isArray(values), "All parameter values must be arrays, see buildUrlSimple for simple values");
      _.each(values, function(value) {
        keyValues.push(encodeURIComponent(key) + "=" + encodeURIComponent(value));
      });
    });

    var queryString = keyValues.join("&");
    return baseUrl + "?" + queryString;
  };

  /**
   * This is a wrapper for buildUrl when all parameters have only one value.
   * It eliminates the need to create one-element arrays.
   *
   * Example:
   *
   * @param url
   * @param urlParameters
   * @returns {*}
   */
  play.buildUrlSimple = function(url, urlParameters) {
    url = url || "";
    urlParameters = urlParameters || {};

    var urlParametersArray = {};

    _.each(urlParameters, function(value, key) {
      Utils.assert(typeof(urlParameters[key])==="string", "All values must be simple strings, for array parameters see buildUrl");
      urlParametersArray[key] = [value];
    });

    return play.buildUrl(url, urlParametersArray);
  };

  /**
   * Use this helper to get a simple value of a single parameter by name from an url.
   * It's a wrapper for parseQueryStringSimple(url)[name] so behaviour is exactly the same for just one parameter.
   * For parameters with multiple values use the most general function parseQueryString.
   *
   * Example:
   *
   * getUrlParameter("http://www.linkedin.com?par1=val1&par2=val2","par1") returns "val1"
   *
   * @param url
   * @param name
   * @return {*}
   */
  play.getUrlParameter = function(url, name) {
    Utils.assert(url, "getUrlParameter called with null url");
    Utils.assert(name, "getUrlParameter called with null parameter name");
    return play.parseQueryStringSimple(url)[name];
  };

  /**
   * Combine two pieces of a URL and ensure that there is only one slash between them.
   *
   * Example:
   *
   * combineUrlPieces("/foo/bar/", "/baz/blah/abc.js")
   *
   * Returns: "/foo/bar/baz/blah/abc.js"
   *
   * @param leftPiece
   * @param rightPiece
   * @return {*}
   */
  play.combineUrlPieces = function(leftPiece, rightPiece) {
    if (!leftPiece) {
      return rightPiece;
    }

    if (!rightPiece) {
      return leftPiece;
    }

    if (/\?$/.test(leftPiece) || /^\?/.test(rightPiece)) {
      return leftPiece + rightPiece;
    } else {
      return leftPiece.replace(/\/$/, '') + "/" + rightPiece.replace(/^\//, '');
    }
  };

  /**
   * Combine multiple parts of a URL to create a single path, ensuring just one slash between any two parts.
   *
   * Example:
   *
   * buildPath("foo", "/bar/", "/baz")
   *
   * Returns: "foo/bar/baz"
   *
   * @returns {*}
   */
  play.buildPath = function() {
    if (arguments.length === 0) {
      return "";
    }

    return _.reduce(arguments, function(acc, path) { return play.combineUrlPieces(acc, path); });
  };

  /**
   * Add the given locale to the given path.
   *
   * Example:
   *
   * addLocale("/foo/bar", "en_US")
   *
   * Returns: "/foo/bar_en_US"
   *
   * @param path
   * @param locale
   * @return {*}
   */
  play.addLocale = function(path, locale) {
    Utils.assert(path, "addLocale called with null or empty path");
    Utils.assert(locale, "addLocale called with null or empty locale");

    return path.endsWith(locale) ? path : path + "_" + locale;
  };

  /**
   * Replace <locale> placeholders with the current locale
   *
   * Example:
   *
   * replaceWithLocale("/foo/<locale>/bar", "en_US")
   *
   * Returns: "/foo/en_US/bar"
   *
   * @param paths
   * @returns {*}
   */
  play.replaceWithLocale = function(paths) {
    Utils.assert(paths, "replaceWithLocale called with null or empty path");

    paths = asArray(paths);
    var locale = play.getPageContextValue("locale", true);
    return _.map(paths, function(path) { return path.replace(/<locale>/g, locale);});
  };

  /**
   * Adds the file extension to path, unless path already ends with that file extension
   *
   * Example:
   *
   * addExtension("/foo/bar", ".js")
   *
   * Returns: "/foo/bar.js"
   *
   * @param path
   * @param ext
   * @return {*}
   */
  play.addExtension = function(path, ext) {
    Utils.assert(path, "addExtension called with null path");
    Utils.assert(ext, "addExtension called with null extension");

    return path.endsWith(ext) ? path : path + ext;
  };

  /**
   * If path ends with extension ext, remove it, otherwise return path unchanged.
   *
   * @param path
   * @param ext
   * @return {*}
   */
  play.removeExtension = function(path, ext) {
    Utils.assert(path, "removeExtension called with null path");
    Utils.assert(ext, "removeExtension called with null extension");

    return path.endsWith(ext) ? path.substring(0, path.length - ext.length) : path;
  };

  /**
   * Extract the values of all "argXXX" keys within given length, where XXX is an int (e.g. arg0, arg1, arg2) and return them as an array.
   *
   * Example:
   *
   * extractUrlArgs({arg0: "foo", arg1: "bar", someOtherThing: "will be ignored"}, 3)
   *
   * Returns: ["foo", "bar"]
   *
   * extractUrlArgs({arg1: "bar", arg2: "baz"}, 3)
   *
   * Returns: [null, "bar", "baz"]
   *
   * @param params
   * @return {Array}
   */
  play.extractUrlArgs = function(params, len) {
    params = params || {};
    var args = [];

    for (var i = 0; i < len; ++i) {
      if (params["arg" + i] !== undefined) {
        args.push(params["arg" + i]);
      } else {
        args.push(null);
      }
    }

    return args;
  };

  /**
   * Play can generate JavaScript reverse routers: that is, JavaScript code that builds URLs to controllers in your
   * routes file. This method finds the reverse router for the alias specified in the params object or throws an
   * exception if a router cannot be found for the given alias.
   *
   * Params can contain:
   *
   * 1. alias: the name of the alias, which should be of the form <controller>.<action> (e.g. controllers.HelloWorld.index)
   * 2. args0, args1, args...: the arguments to pass to the action method to generate the URL
   *
   * The returned router will have url and absoluteUrl properties.
   *
   * @param params
   * @return {*}
   */
  play.reverseRouterForAlias = function(params) {
    Utils.assert(params, "reverseRouterForAlias called with null params object");
    Utils.assert(params.alias, "reverseRouterForAlias called with a params object that does not define an alias");
    Utils.assert(play.jsRoutes, 'Could not find any JavaScript reverse routers. Did you define any in your DustOptions object?');

    var alias = params.alias;
    var route = play.traverseObject(play.jsRoutes, alias);
    var args = params.args || play.extractUrlArgs(params, route.length);

    args = args || [];
    var argList = args instanceof Array ? args : [args];

    Utils.assert(route, 'Could not find alias ' + alias + '. Make sure to define the proper JavaScriptRoutes in your DustOptions object.');

    return route.apply(route, argList);
  };

  /**
   * Use the reverseRouterForAlias function to find the JavaScript reverse router for the alias specified in params and
   * use it to generate a URL. If params.withCsrf is specified, a CSRF token will be appended to the URL.
   *
   * @param params
   * @param {boolean} [absolute=false] If true, an absolute URL will be returned
   * @return {*}
   */
  play.url = function(params, absolute) {
    absolute = absolute || false;

    Utils.assert(params, "url called with null params object");

    var reverseRoute = play.reverseRouterForAlias(params);
    var url = _.result(reverseRoute, absolute ? 'absoluteURL' : 'url');

    if (params.withCsrf) {
      url = play.addCsrfTokenToUrl(url);
    }

    // Allow track or trk because the naming is sometimes confusing.
    var track = params.track || params.trk;
    if (track) {
      url = play.addQueryParameter(url, "trk", track);
    }

    return url;
  };

  /**
   * Build a URL to the static asset(s) specified by paths. If paths is an array of multiple paths, you will get a
   * dynamically concatenated resource. In dev mode, this will go back to your Play app. In prod, this will go via the
   * CDN to spark.
   *
   * @param paths a single path (String) or multiple
   * @return {String}
   */
  play.assetUrl = function(paths) {
    Utils.assert(paths, "assetUrl called with null paths");
    paths = asArray(paths);
    checkUrlLength(paths);

    var oldAssetUrlRouter = play.traverseObject(play.jsRoutes, "controllers.Assets");
    var newAssetUrlRouter = play.traverseObject(play.jsRoutes, "com.linkedin.assets.AssetsController");
    var appName = play.getPageContextValue("appName", true);

    //cdn urls exits only when we are in prod mode
    if (play.useCdn()) {
      return play.sparkUrl(paths);
    } else if (oldAssetUrlRouter) {
      return oldAssetUrlRouter.at(paths).url;
    } else if (newAssetUrlRouter) {
      return newAssetUrlRouter.at(formatPathsForSpark(appName, paths)).url;
    } else {
      return play.combineUrlPieces(play.getPageContextValue("baseAssetsUrl", true), formatPathsForSpark(appName, paths));
    }
  };

  /**
   * Build a URL back to spark for static content for the given paths. If paths has more than one file in it, you will
   * get a dynamically concatenated resource. In dev mode, this URL will go back to the Play app, which will act as a
   * fake spark and serve files from the file system. In prod mode, the URL will be a CDN URL that goes to spark.
   * If static content hashes of all paths exist in the global namespace, the result URL will be hash based.
   *
   * @param paths
   * @returns {*}
   */
  play.sparkUrl = function(paths) {
    Utils.assert(paths, "sparkUrl called with null paths");
    var appName = play.getPageContextValue("appName", true);

    var hashNotFound = function (path) {
      var result = !Utils.isDefined(sc.hashes[appName][path]);
      return result;
    };

    if (play.hashesDisabledByQueryParam() || !Utils.isDefined(sc.hashes[appName]) || _.some(paths, hashNotFound)) {
      return versionedSparkUrl(appName, paths);
    } else {
      return hashedSparkUrl(appName, paths);
    }
  };

  /**
   * Build a URL to the "unversioned" static content directory of SCDS. This will be a relative URL in dev and a CDN URL
   * in prod.
   *
   * Example:
   *
   * scdsDirectUnversionedUrl("foo.js")
   *
   * Returns: /scds/common/u/foo.js
   *
   * @param path
   * @return {*}
   */
  play.scdsDirectUnversionedUrl = function(path) {
    Utils.assert(path, "scdsDirectUnversionedUrl called with null path");

    return play.scdsDirectUrl(play.combineUrlPieces('common/u/', path));
  };

  /**
   * Build a URL to SCDS. This will be a relative URL in dev and a CDN URL in prod.
   *
   * Example:
   *
   * scdsDirectUrl("foo.js")
   *
   * Returns: /scds/foo.js
   *
   * @param path
   * @return {*}
   */
  play.scdsDirectUrl = function(path) {
    Utils.assert(path, "scdsDirectUrl called with null path");

    if (/^http/.test(path)) {
      return path;
    } else {
      return play.combineUrlPieces(play.getPageContextValue("baseScdsUrl", true), path);
    }
  };

  /**
   * For relative urls, append the cdn url. For absolute urls, keep them as they are. Note: in dev mode, the cdn url
   * is typically an empty string, so the URL will remain a relative URL.
   *
   * @param path
   * @return {*}
   */
  play.appendCdnUrlIfNeeded = function(path) {
    Utils.assert(path, "appendCdnUrlIfNeeded called with null path");

    if (/^http/.test(path)) {
      return path;
    } else {
      return play.combineUrlPieces(play.getPageContextValue("cdnUrl", true), path);
    }
  };

  /**
   * Create a URL for the given path(s) that will include the current locale and given extension. If paths is an array,
   * the locale will be added to each path in it, and the URL will be to a dynamically concatenated resource. If a path
   * already had an extension, it will be replaced.
   *
   * Example:
   *
   * localizedAssetUrl("/foo/bar.js", ".css")
   *
   * Output:
   *
   * "/foo/bar_en_US.css"
   *
   * @param paths
   * @param extension
   * @return {*}
   */
  play.localizedAssetUrl = function(paths, extension) {
    Utils.assert(paths, "localizedAssetUrl called with null or empty paths");

    paths = asArray(paths);

    var locale = play.getPageContextValue("locale", true);
    var pathsWithLocale = _.map(paths, function(path) { return play.addExtension(play.addLocale(play.removeExtension(path, extension), locale), extension); });

    return play.assetUrl(pathsWithLocale);
  };

  /**
   * Create the URL for the given dust template path(s) under public/templates. This method will automatically add the
   * locale and the .js extension to the URL. If paths is an array, the locale will be added to each path in it and
   * the URL will be to a dynamically concatenated resource.
   *
   * Example:
   *
   * templateUrl("foo")
   *
   * Returns: "/assets/templates/foo_en_US.js"
   *
   * @param paths
   * @return {*}
   */
  play.templateUrl = function(paths) {
    Utils.assert(paths, "templateUrl called with null or empty paths");

    paths = asArray(paths);
    var pathsWithPrefix = _.map(paths, function(path) {
      return (path.startsWith("templates") || path.startsWith("scmp")) ? path : play.combineUrlPieces("templates", path);
    });
    return play.localizedAssetUrl(pathsWithPrefix, ".js");
  };

  /**
   * Create the URL for build-time concatenated css path(s) under public/concat. This method will automatically add the
   * locale and the .css extension to the URL. If paths is an array, the locale and extension will be added to each path
   * in it and the URL will be to a dynamically concatenated resource.
   *
   * Example:
   *
   * concatCssUrl("foo/bar")
   *
   * Output:
   *
   * "/assets/concat/foo/bar.css"
   *
   * @param paths
   * @return {*}
   */
  play.concatCssUrl = function(paths) {
    Utils.assert(paths, "concatCssUrl called with null or empty paths");

    paths = asArray(paths);
    var pathsWithPrefix = _.map(paths, function(path) { return play.combineUrlPieces("concat", path); });
    return play.localizedAssetUrl(pathsWithPrefix, ".css");
  };

  /**
   * Create the URL for build-time concatenated js path(s) under public/concat. This method will automatically add the
   * locale and the .js extension to the URL. If paths is an array, the locale and extension will be added to each path
   * in it and the URL will be to a dynamically concatenated resource.
   *
   * Example:
   *
   * concatJsUrl("foo/bar")
   *
   * Output:
   *
   * "/assets/concat/foo/bar.js"
   *
   * @param paths
   * @return {*}
   */
  play.concatJsUrl = function(paths) {
    Utils.assert(paths, "concatJsUrl called with null or empty paths");

    paths = asArray(paths);
    var pathsWithPrefix = _.map(paths, function(path) { return play.combineUrlPieces("concat", path); });
    return play.localizedAssetUrl(pathsWithPrefix, ".js");
  };

  /**
   * Returns a URL to the JavaScript path(s) under public/javascripts. The .js extension will be added automatically. If
   * paths is an array, the extension will be added to each path in it and the URL will be to a dynamically concatenated
   * resource.
   *
   * @param paths
   * @return {*}
   */
  play.jsUrl = function(paths) {
    Utils.assert(paths, "jsUrl called with null paths");

    paths = asArray(paths);
    var pathsWithPrefix = _.map(paths, function(path) { return play.combineUrlPieces("javascripts", play.addExtension(path, '.js')); });
    return play.assetUrl(pathsWithPrefix);
  };

  /**
   * Returns a URL to the CSS path(s) under public/stylesheets. The .css extension will be added automatically. If paths
   * is an array, the extension will be added to each path in it and the URL will be to a dynamically concatenated
   * resource.
   *
   * @param paths
   * @return {*}
   */
  play.cssUrl = function(paths) {
    Utils.assert(paths, "cssUrl called with null path");

    paths = asArray(paths);
    var pathsWithPrefix = _.map(paths, function(path) { return play.combineUrlPieces("stylesheets", play.addExtension(path, '.css')); });
    return play.assetUrl(pathsWithPrefix);
  };

  /**
   * Returns a URL to the CSS path(s) compiled from SCSS under public/scss. The .css or scss extension is optional. If
   * paths is an array, the extension will be added to each path in it and the URL will be to a dynamically concatenated
   * resource.
   *
   * @param paths
   * @return {*}
   */
  play.scssUrl = function(paths) {
    Utils.assert(paths, "scssUrl called with null path");

    paths = asArray(paths);
    var pathsWithPrefix = _.map(paths, function(path) {
      var withoutExtension = play.removeExtension(path, ".scss");
      return play.combineUrlPieces("scss", withoutExtension);
    });


    return play.localizedAssetUrl(pathsWithPrefix, ".css");
  };

  /**
   * Returns a URL to an image file under public/images.
   *
   * @param path
   * @return {*}
   */
  play.imgUrl = function(path) {
    Utils.assert(path, "imgUrl called with null path");

    return play.assetUrl('images/' + path);
  };

  /**
   * Returns a URL to an image hosted by the mpr service.
   *
   * @param mediaId
   * @param width
   * @param height
   * @return {*}
   */
  play.mprUrl = function(mediaId, width, height) {
    Utils.assert(mediaId, "mprUrl called with null mediaId");
    Utils.assert(width, "mprUrl called with null width/size");

    var imageWidth = width;
    var imageHeight = height || width;
    return play.combineUrlPieces(play.getPageContextValue("baseMprUrl", true), 'shrink_' + imageWidth + '_' + imageHeight + mediaId);
  };

  /**
   * Returns a URL to an image via the media path.
   *
   * @param mediaId
   * @return {*}
   */
  play.mediaUrl = function(mediaId) {
    Utils.assert(mediaId, "mediaUrl called with null mediaId");

    return play.combineUrlPieces(play.getPageContextValue('baseMediaUrl', true), mediaId);
  };

  /**
   * Returns a URL to a "no photo" ghost profile image
   *
   * @param width
   * @param height
   * @return {*}
   */
  play.noPhotoUrl = function(width, height) {
    Utils.assert(width, "noPhotoUrl called with null width/size");

    var imageWidth = width;
    var imageHeight = height || width;
    return play.scdsDirectUnversionedUrl('/images/themes/katy/ghosts/person/ghost_person_' + imageWidth + 'x' + imageHeight + '_v1.png');
  };

  /**
   * Extracts a list of paths from params. The list will contain all entries in params.path and params.paths; the
   * latter can contain a comma separated list that will be automatically split into separate entries.
   *
   * Example:
   *
   * getPathList({path: "foo", paths: "bar,baz,blah"})
   *
   * Returns: ["foo", "bar", "baz", "blah"]
   *
   * @param params
   * @return {Object}
   */
  play.getPathList = function(params) {
    params = params || {};

    var allPaths = [];

    if (params.path) {
      allPaths.push(params.path);
    }

    if (params.paths) {
      allPaths = allPaths.concat(Array.isArray(params.paths) ? params.paths : params.paths.split(","));
    }

    return _.map(allPaths, function(path) { return path.trim(); });
  };

  /**
   * Extract mediaId from URN
   *
   * Example
   * Input: "urn:li:media:/p/1/000/000/1b4/1305a66.png"
   * Output: "/p/1/000/000/1b4/1305a66.png"
   *
   * @param urn
   * @return {String}
   */
  play.getMediaIdFromUrn = function(urn) {
    var indexOfUrnMediaPrefix = urn.lastIndexOf(play.MEDIA_URN_PREFIX);

    Utils.assert(indexOfUrnMediaPrefix > -1, "Incorrectly formatted URN");

    return urn.slice(indexOfUrnMediaPrefix + play.MEDIA_URN_PREFIX.length);
  };

  /**
   * Extract mediaId from a params object, returns params.mediaId if present
   * otherwise the mediaId is extracted from params.urn. If neither returns undefined.
   *
   * @param params
   * @returns {*}
   */
  play.getMediaIdFromParams = function(params) {
    Utils.assert(params, "getMediaIdFromParams called with null params");

    if(params.mediaId) {
      return params.mediaId;
    } else if(params.urn) {
      return play.getMediaIdFromUrn(params.urn);
    } else {
      return undefined;
    }
  };

  /**
   * Writes the URL for the asset(s) specified in the 'path' or 'paths' parameters. If multiple paths are specified,
   * the URL will be for a dynamically concatenated resource.
   *
   * Example:
   *
   * {@assetUrl path="foo.js"/}
   * {@assetUrl paths="foo.js, bar.js"/}
   *
   * Output:
   *
   * /dust-sample/assets/foo.js
   * /dust-sample/assets/foo.js,bar.js
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.assetUrl = function(chunk, context, bodies, params) {
    Utils.assert(params, "@assetUrl called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(dust.escapeHtml(play.assetUrl(paths)));
  };

  /**
   * Writes the URL for the dust template path(s) under public/templates. Includes the locale and extension
   * automatically. If multiple paths are specified, the URL will be for a dynamically concatenated resource.
   *
   * Example:
   *
   * {@templateUrl path="foo"/}
   * {@templateUrl path="foo, bar"/}
   *
   * Output:
   *
   * /assets/templates/foo_en_US.js
   * /assets/templates/foo_en_US.js,bar_en_US.js
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.templateUrl = function(chunk, context, bodies, params) {
    Utils.assert(params, "@templateUrl called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(dust.escapeHtml(play.templateUrl(paths)));
  };

  /**
   * Writes the URL for the build-time concatenated css path(s) under public/concat. Includes the locale and extension
   * automatically. If multiple paths are specified, the URL will be for a dynamically concatenated resource.
   *
   * Example:
   *
   * {@concatCssUrl path="foo"/}
   *
   * Output:
   *
   * /assets/concat/foo_en_US.css
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.concatCssUrl = function(chunk, context, bodies, params) {
    Utils.assert(params, "@concatCssUrl called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(dust.escapeHtml(play.concatCssUrl(paths)));
  };

  /**
   * Writes the URL for the build-time concatenated js path(s) under public/concat. Includes the locale and extension
   * automatically. If multiple paths are specified, the URL will be for a dynamically concatenated resource.
   *
   * Example:
   *
   * {@concatJsUrl path="foo"/}
   *
   * Output:
   *
   * /assets/concat/foo_en_US.js
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.concatJsUrl = function(chunk, context, bodies, params) {
    Utils.assert(params, "@concatJsUrl called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(dust.escapeHtml(play.concatJsUrl(paths)));
  };

  /**
   * Creates a script tag for the linkedin-dust JavaScript library.
   *
   * Example:
   *
   * {@linkedInDustScriptTag/}
   *
   * Output:
   *
   * <script src="/assets/dust/dev/linkedin-dust.js"></script>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.linkedInDustScriptTag = function(chunk, context, bodies, params) {
    return chunk.write(play.createScriptTag({src: play.getPageContextValue("linkedInDustJsUrl", true)}, params || {}));
  };

  /**
   * Writes a script tag with the URL for the dust template path(s) under public/templates. If more than one path is
   * specified, the URL will be for a dynamically concatenated resource.
   *
   * The .js extension is optional: the helper will add it for you. Any parameter other than 'path' and 'paths' will be
   * passed unchanged to the script tag.
   *
   * Examples:
   *
   * {@template path="foo"/}
   * {@template paths="bar, a/b/c" id="bar"/}
   *
   * Output:
   *
   * <script type="text/javascript" src="/assets/templates/foo_en_US.js"></script>
   * <script type="text/javascript" src="/assets/templates/bar_en_US.js,/assets/templates/a/b/c_en_US.js" id="bar"></script>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.template = function(chunk, context, bodies, params) {
    Utils.assert(params, "@template called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(play.createScriptTag({src: play.templateUrl(paths)}, params));
  };

  /**
   * Writes a link tag with the URL for the build-time concatenated css path(s) under public/concat. If more than one
   * path is specified, the URL will be for a dynamically concatenated resource.
   *
   * The .css extension is optional: the helper will add it for you. Any parameter other than 'path' and 'paths' will be
   * passed unchanged to the script tag. The URLs will automatically include the proper locale.
   *
   * Examples:
   *
   * {@concatCss path="foo"/}
   *
   * Output:
   *
   * <link rel="stylesheet" href="/assets/concat/foo_en_US.css"/>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.concatCss = function(chunk, context, bodies, params) {
    Utils.assert(params, "@concatCss called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(play.createCssTag({href: play.concatCssUrl(paths)}, params));
  };

  /**
   * Writes a script tag with the URL for the build-time concatenated js path(s) under public/concat. If more than one
   * path is specified, the URL will be for a dynamically concatenated resource.
   *
   * The .js extension is optional: the helper will add it for you. Any parameter other than 'path' and 'paths' will be
   * passed unchanged to the script tag. The URLs will automatically include the proper locale.
   *
   * Examples:
   *
   * {@concatJs path="foo"/}
   *
   * Output:
   *
   * <script type="text/javascript" src="/assets/concat/foo_en_US.js"></script>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.concatJs = function(chunk, context, bodies, params) {
    Utils.assert(params, "@concatJs called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(play.createScriptTag({src: play.concatJsUrl(paths)}, params));
  };

  /**
   * Writes a script tag with the URL for the JS path(s) specified in the 'path' or 'paths' parameter. If more than one
   * path is specified, the URL will be for a dynamically concatenated resource. If the path contains <locale> placeholders,
   * they will be replaced by the current locale. This is useful if you only want some of the js be localized.
   * All paths are assumed to be relative to /public/javascripts.
   *
   * The .js extension is optional: the helper will add it for you. Any parameter other than 'path' and 'paths' will be
   * passed unchanged to the script tag.
   *
   * Examples:
   *
   * {@js path="jquery"/}
   * {@js paths="dust, controllers/foo" id="bar"/}
   * {@js paths="dust, lib/t8/<locale>/t8.js" id="bar"/}
   *
   * Output:
   *
   * <script type="text/javascript" src="/assets/javascripts/jquery.js"></script>
   * <script type="text/javascript" src="/assets/javascripts/dust.js,/assets/javascripts/controllers/foo.js" id="bar"></script>
   * <script type="text/javascript" src="/assets/javascripts/dust.js,/assets/lib/t8/en_US/t8.js" id="bar"></script>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.js = function(chunk, context, bodies, params) {
    Utils.assert(params, "@js called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.replaceWithLocale(play.getPathList(params));

    return chunk.write(play.createScriptTag({src: play.jsUrl(paths)}, params));
  };

  /**
   * Writes a script tag with the URL for the JS path(s) specified in the 'path' or 'paths' parameter. If more than one
   * path is specified the URL will be for a dynamically concatenated resource. The URL will  include the current
   * locale. All paths are assumed to be relative to /public/javascripts.
   *
   * The .js extension is optional: the helper will add it for you. Any parameter other than 'path' and 'paths' will be
   * passed unchanged to the script tag.
   *
   * Examples:
   *
   * {@jsLocalized path="jquery"/}
   * {@jsLocalized paths="dust, controllers/foo" id="bar"/}
   *
   * Output:
   *
   * <script type="text/javascript" src="/assets/javascripts/jquery_en_US.js"></script>
   * <script type="text/javascript" src="/assets/javascripts/dust_en_US.js,/assets/javascripts/controllers/foo_en_US.js" id="bar"></script>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.jsLocalized = function(chunk, context, bodies, params) {
    Utils.assert(params, "@jsLocalized called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(play.createScriptTag({src: play.localizedAssetUrl(paths, '.js')}, params));
  };

  /**
   * Writes a script tag with the URL for the JS path(s) specified in the 'path' or 'paths' parameter. If more than one
   * path is specified, the URL will be for a dynamically concatenated resource. If the path contains <locale> placeholders,
   * they will be replaced by the current locale. This is useful if you only want some of the js be localized. Unlike @js,
   * all paths are assumed to be relative to /public/ (ie, the /javascripts path is NOT assumed).
   *
   * The .js extension is optional: the helper will add it for you. Any parameter other than 'path' and 'paths' will be
   * passed unchanged to the script tag.
   *
   * Examples:
   *
   * {@jsAsset path="foo"/}
   * {@jsAsset paths="bar, /home/dashboard/charts" id="bar"/}
   * {@jsAsset paths="lib/t8/<locale>/t8.js, /home/dashboard/charts" id="bar"/}
   *
   * Output:
   *
   * <script type="text/javascript" src="/assets/foo.js"></script>
   * <script type="text/javascript" src="/assets/bar.js,/home/dashboard/charts.js" id="bar"></script>
   * <script type="text/javascript" src="/assets/lib/t8/en_US/t8.js,/home/dashboard/charts.js" id="bar"></script>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.jsAsset = function(chunk, context, bodies, params) {
    Utils.assert(params, "@jsAsset called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.replaceWithLocale(play.getPathList(params));
    var jsPaths = _.map(paths, function(path) { return play.addExtension(path, '.js'); });

    return chunk.write(play.createScriptTag({src: play.assetUrl(jsPaths)}, params));
  };

  /**
   * Writes a script tag with the URL for the JS path(s) specified in the 'path' or 'paths' parameter. If more than one
   * path is specified, the URL will be to a dynamically concatenated resource. The URL  will include the the current locale.
   * Unlike @jsLocalized, all paths are assumed to be relative to /public/ (ie, the /javascripts path is NOT assumed).
   *
   * The .js extension is optional: the helper will add it for you. Any parameter other than 'path' and 'paths' will be
   * passed unchanged to the script tag.
   *
   * Examples:
   *
   * {@jsAssetLocalized path="foo"/}
   * {@jsAssetLocalized paths="bar, /home/dashboard/charts" id="bar"/}
   *
   * Output:
   *
   * <script type="text/javascript" src="/assets/foo_en_US.js"></script>
   * <script type="text/javascript" src="/assets/bar_en_US.js,/assets/home/dashboard/charts_en_US.js" id="bar"></script>

   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.jsAssetLocalized = function(chunk, context, bodies, params) {
    Utils.assert(params, "@jsAssetLocalized called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(play.createScriptTag({src: play.localizedAssetUrl(paths, '.js')}, params));
  };

  /**
   * Writes a link tag with the URL for the CSS path(s) specified in the 'path' or 'paths' parameter. If more than one
   * path is specified, the URL will be to a dynamically concatenated resource. All paths are assumed to be relative to
   * /public/stylesheets.
   *
   * The .css extension is optional: the helper will add it for you. Any parameter other than 'path' and 'paths' will be
   * passed unchanged to the link tag.
   *
   * Examples:
   *
   * {@css path="main"/}
   * {@css paths="common, pages/home" media="screen"/}
   *
   * Output:
   *
   * <link rel="stylesheet" href="/assets/stylesheets/main.css"/>
   * <link rel="stylesheet" href="/assets/stylesheets/common.css,/assets/stylesheets/pages/home.css" media="screen"/>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.css = function(chunk, context, bodies, params) {
    Utils.assert(params, "@css called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(play.createCssTag({href: play.cssUrl(paths)}, params));
  };

  /**
   * Writes a link tag with the URL for the SCSS path(s) specified in the 'path' or 'paths' parameter. If more than one
   * path is specified, the URL will be to a dynamically concatenated resource. All paths are assumed to be relative to
   * /public/scss.
   *
   * The .scss extension is optional: the helper will add it for you. Any parameter other than 'path' and 'paths' will be
   * passed unchanged to the link tag.
   *
   * Examples:
   *
   * {@scss path="main"/}
   * {@scss paths="common, pages/home" media="screen"/}
   *
   * Output:
   *
   * <link rel="stylesheet" href="/assets/scss/main.css"/>
   * <link rel="stylesheet" href="/assets/scss/common.css,/assets/scss/pages/home.css" media="screen"/>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.scss = function(chunk, context, bodies, params) {
    Utils.assert(params, "@scss called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(play.createCssTag({href: play.scssUrl(paths)}, params));
  };

  /**
   * Writes a link tag with the URL for the CSS path(s) specified in the 'path' or 'paths' parameter. If more than one
   * path is specified, the URL will be to a dynamically concatenated resource. Unlike @css, all paths are assumed to
   * be relative to /public/ (ie, the /css path is NOT assumed).
   *
   * The .css extension is optional: the helper will add it for you. Any parameter other than 'path' and 'paths' will be
   * passed unchanged to the link tag.
   *
   * Examples:
   *
   * {@cssAsset path="home/main"/}
   * {@cssAsset paths="common, pages/dashboard/home" media="screen"/}
   *
   * Output:
   *
   * <link rel="stylesheet" href="/assets/home/main.css"/>
   * <link rel="stylesheet" href="/assets/common.css,/assets/pages/dashboard/home.css" media="screen"/>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.cssAsset = function(chunk, context, bodies, params) {
    Utils.assert(params, "@cssAsset called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);
    var cssPaths = _.map(paths, function(path) { return play.addExtension(path, '.css'); });

    return chunk.write(play.createCssTag({href: play.assetUrl(cssPaths)}, params));
  };

  /**
   * Writes a link tag with the URL for the CSS path(s) specified in the 'path' or 'paths' parameter. If more than one
   * path is specified, the URL will be to a dynamically concatenated resource. The URL will include the current locale
   * appended. Unlike @css, all paths are assumed to be relative to /public/ (ie, the /css path is NOT assumed).
   *
   * The .css extension is optional: the helper will add it for you. Any parameter other than 'path' and 'paths' will be
   * passed unchanged to the link tag.
   *
   * Examples:
   *
   * {@cssAssetLocalized path="home/main"/}
   * {@cssAssetLocalized paths="common, pages/dashboard/home" media="screen"/}
   *
   * Output:
   *
   * <link rel="stylesheet" href="/assets/home/main_en_US.css"/>
   * <link rel="stylesheet" href="/assets/common_en_US.css,/assets/pages/dashboard/home_en_US.css" media="screen"/>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.cssAssetLocalized = function(chunk, context, bodies, params) {
    Utils.assert(params, "@cssAssetLocalized called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    var paths = play.getPathList(params);

    return chunk.write(play.createCssTag({href: play.localizedAssetUrl(paths, '.css')}, params));
  };

  /**
   * Writes an img tag with the URL specified in the 'path' parameter. The path is assumed to be relative to
   * /public/images. Any parameter other than 'path' will be passed unchanged to the img tag.
   *
   * Examples:
   *
   * {@img path="logo.png"/}
   * {@img path="icons/like.gif" alt="Like"/}
   *
   * Output:
   *
   * <img src="/assets/images/logo.png"/>
   * <img src="/assets/images/icons/like.gif" alt="Like"/>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.img = function(chunk, context, bodies, params) {
    Utils.assert(params, "@img called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    var attrs = _.extend({src: play.imgUrl(params.path)}, _.omit(params, ['path']));
    return chunk.write(HtmlUtils.createHtmlTag("img", attrs, null));
  };

  /**
   * Writes an img tag for an image hosted on scds with path 'path'. The image comes from SCDS's "unversioned" URLs.
   * Any parameter other than 'path' will be passed unchanged to the img tag.
   *
   * Example:
   *
   * {@scdsImg path="img/foo/bar.png" class="baz"/}
   *
   * Output:
   *
   * <img src="/scds/common/u/img/foo/bar.png" class="baz"/>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.scdsImg = function(chunk, context, bodies, params) {
    Utils.assert(params, "@scdsImg called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    var attrs = _.extend({src: play.scdsDirectUnversionedUrl(params.path)}, _.omit(params, ['path']));
    return chunk.write(HtmlUtils.createHtmlTag("img", attrs, null));
  };

  /**
   * Writes a script tag for JS hosted on scds with path 'path'. The JS comes from SCDS's "unversioned" URLs.
   * Any parameter other than 'path' will be passed unchanged to the script tag.
   *
   * Example:
   *
   * {@scdsJs path="lib/foo/bar"/}
   *
   * Output:
   *
   * <script src="/scds/common/u/lib/foo/bar.js"></script>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.scdsJs = function(chunk, context, bodies, params) {
    Utils.assert(params, "@scdsJs called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    return chunk.write(play.createScriptTag({src: play.scdsDirectUnversionedUrl(play.addExtension(params.path, '.js'))}, params));
  };

  /**
   * Builds a SCDS "unversioned" URL with the passed in 'path'.
   *
   * Example:
   *
   * {@scdsUrl path="lib/foo/bar.js"/}
   *
   * Output:
   *
   * /scds/common/u/lib/foo/bar.js
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.scdsUrl = function(chunk, context, bodies, params) {
    Utils.assert(params, "@scdsUrl called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    return chunk.write(dust.escapeHtml(play.scdsDirectUnversionedUrl(params.path)));
  };

  /**
   * Writes a link tag for CSS hosted on scds with path 'path'. The CSS comes from SCDS's "unversioned" URLs.
   * Any parameter other than 'path' will be passed unchanged to the link tag.
   *
   * Example:
   *
   * {@scdsCss path="foo/bar"/}
   *
   * Output:
   *
   * <link href="/scds/common/u/foo/bar.css"></script>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.scdsCss = function(chunk, context, bodies, params) {
    Utils.assert(params, "@scdsCss called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    return chunk.write(play.createCssTag({href: play.scdsDirectUnversionedUrl(play.addExtension(params.path, '.css'))}, params));
  };

  /**
   * Writes an img tag with for the mpr image specified by the 'mediaId' or 'urn' params (if both are specified, mediaId
   * will be used). The size of the image is determined by the 'size' or 'width' and 'height' params (if both are
   * specified, size will be used). All other parameters will be passed unchanged to the img tag.
   *
   * Example:
   *
   * {@mprImg mediaId="/p/000/001/profile-photo.png" width="200" height="100" alt="My profile photo"/}
   * {@mprImg urn="urn:li:media:/p/1/000/000/1b4/1305a66.png" width="100" height="60" class="logo" /}
   *
   * Output:
   *
   * <img src="/mpr/mpr/shrink_80_80/p/000/001/profile-photo.png" width="200" height="100" alt="My profile photo"/>
   * <img src="/mpr/mpr/shrink_100_60/p/1/000/000/1b4/1305a66.png" width="100" height="60" class="logo"/>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.mprImg = function(chunk, context, bodies, params) {
    Utils.assert(params, "@mprImg called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    var imageWidth = params.size || params.width;
    var imageHeight = params.size || params.height;

    var attrs = _.extend(
      {src: play.mprUrl(play.getMediaIdFromParams(params), imageWidth, imageHeight)},
      _.omit(params, ['mediaId', 'urn', 'size', 'width', 'height', 'imageWidth', 'imageHeight'])
    );

    if(imageWidth) {
      attrs.width = parseInt(imageWidth, 10) + '';
    }

    if(imageHeight) {
      attrs.height = parseInt(imageHeight, 10) + '';
    }

    return chunk.write(HtmlUtils.createHtmlTag('img', attrs, null));
  };

  /**
   * Writes an img tag with for the profile photo specified by the 'mediaId' or 'urn' params (if both are specified,
   * mediaId will be used). If neither 'mediaId' nor 'urn' can be found, shows the 'no-photo' silhouette instead. The
   * size of the image is determined by the 'size' or 'width' and 'height' params (if both are specified, size will be
   * used). All other parameters will be passed unchanged to the img tag.
   *
   * Examples:
   *
   * {@profileImg mediaId="/p/000/001/profile-photo.png" width="80" height="80" alt="My profile photo"/}
   * {@profileImg urn="urn:li:media:/p/1/000/000/2cc/004eb3a.jpg" width="100" height="100"/}
   * {@profileImg mediaId=undefinedVariable width="40" height="40" alt="Should show no photo"/}
   *
   * Output:
   *
   * <img src="/mpr/mpr/shrink_80_80//p/000/001/profile-photo.png" width="80" height="80" alt="My profile photo"/>
   * <img src="/mpr/mpr/shrink_100_100/p/1/000/000/2cc/004eb3a.jpg" width="100" height="100"/>
   * <img src="/scds/common/u/img/icon/icon_no_photo_no_border_40x40.png" width="40" height="40" alt="Should show no photo"/>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.profileImg = function(chunk, context, bodies, params) {
    Utils.assert(params, "@profileImg called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    var imageWidth = params.size || params.width;
    var imageHeight = params.size || params.height;
    var mediaId = play.getMediaIdFromParams(params);

    var src = mediaId ? play.mprUrl(mediaId, imageWidth, imageHeight) : play.noPhotoUrl(imageWidth, imageHeight);
    var attrs = _.extend({src: src}, _.omit(params, ['mediaId', 'urn', 'size', 'width', 'height']));

    if(imageWidth) {
      attrs.width = parseInt(imageWidth, 10) + '';
    }

    if(imageHeight) {
      attrs.height = parseInt(imageHeight, 10) + '';
    }

    return chunk.write(HtmlUtils.createHtmlTag("img", attrs, null));
  };

  /**
   * Writes an img tag for the image specified by the urn or mediaId directly from /media (not from /mpr).
   *
   * Examples:
   *
   * {@mediaImg mediaId="/p/000/001/profile-photo.png" alt="My profile photo"/}
   * {@mediaImg urn="urn:li:media:/p/1/000/000/196/3283598.png" class="logo" /}
   *
   * Output:
   *
   * <img src="/media/p/000/001/profile-photo.png" alt="My profile photo"/>
   * <img src="/media/p/1/000/000/196/3283598.png" class="logo"/>
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.mediaImg = function(chunk, context, bodies, params) {
    Utils.assert(params, "@mediaImg called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    var attrs = _.extend({src: play.mediaUrl(play.getMediaIdFromParams(params))}, _.omit(params, ['mediaId', 'urn']));
    return chunk.write(HtmlUtils.createHtmlTag('img', attrs, null));
  };

  /**
   * Write the URL specified by the 'alias' and (optional) 'arg0', 'arg1', etc. parameters.
   *
   * Examples:
   *
   * {@url alias="controllers.HelloWorld.index"/}
   * {@url alias="controllers.Helpers.threeParams" arg0=firstName arg1=lastName arg2=age/}
   * {@url alias="controllers.Helpers.index" track="some-track"/}
   *
   * Output:
   *
   * /dust-sample
   * /dust-sample/helpers/threeParams?firstName=Jim&lastName=Brikman&age=28
   * /dust-sample?trk=some-track
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @return {*}
   */
  dust.helpers.url = function(chunk, context, bodies, params) {
    Utils.assert(params, "@url called with null params");
    params = dust.helpers.tapAll(params, chunk, context);

    return chunk.write(dust.escapeHtml(play.url(params, params.absolute === 'true')));
  };

  /**
   * Adds a key-value pair to the querystring of a URL, overwriting the
   * existing value if one by that name already exists.
   *
   * Examples:
   * {@addQueryParameter url="/foo" key="bar" value="baz"/}
   * {@addQueryParameter url="/foo?bar=baz" key="biz" value="buzz"/}
   * {@addQueryParameter url="/foo?bar=baz" key="bar" value="biz"/}
   *
   * Output:
   * /foo?bar=baz
   * /foo?bar=baz&biz=buzz
   * /foo?bar=biz
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @returns {*}
   */
  dust.helpers.addQueryParameter = function addQueryParameterHelper(chunk, context, bodies, params) {
    Utils.assert(params, "@addQueryParameter called with null params");
    Utils.assert(params.url, "@addQueryParameter called without URL param");
    Utils.assert(params.key, "@addQueryParameter called without key name");
    Utils.assert(params.value, "@addQueryParameter called without value for key");
    params = dust.helpers.tapAll(params, chunk, context);

    return chunk.write(dust.escapeHtml(play.addQueryParameter(params.url, params.key, params.value)));
  };

  /**
   * Adds a tracking code to a URL.
   *
   * Example:
   * {@addTrackingCode url="/bar" code="some-track-code"/}
   *
   * Output:
   * /bar?trk=some-track-code
   *
   * @param chunk
   * @param context
   * @param bodies
   * @param params
   * @returns {*}
   */
  dust.helpers.addTrackingCode = function(chunk, context, bodies, params) {
    Utils.assert(params, "@addTracking called with null params");
    Utils.assert(params.url, "@addTracking called without url");
    Utils.assert(params.code, "@addTracking called without tracking code");
    params = dust.helpers.tapAll(params, chunk, context);

    return dust.helpers.addQueryParameter(chunk, context, bodies, {
      key: "trk",
      value: params.code,
      url: params.url
    });
  };

})(play, LI, dust, t8, sc, _);


/**
 * Utility JavaScript functions that are not related to dust, Play, or LinkedIn. Many of these exist in jQuery or
 * underscore, but we don't want the Play code to bring in these dependencies and have to deal with all sorts of
 * conflicts.
 */
(function(play, t8, root){

  "use strict";

  var Utils = t8.Utils;

  /**
   * IE doesn't have indexOf for Array:  https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/IndexOf
   *
   */
  if (!Array.prototype.indexOf) {
    Array.prototype.indexOf = function(searchElement /*, fromIndex */ ) {
      if (this === null) {
        throw new TypeError();
      }
      var t = Object(this);
      var len = t.length >>> 0;
      if (len === 0) {
        return -1;
      }
      var n = 0;
      if (arguments.length > 1) {
        n = Number(arguments[1]);
        if (n !== n) { // shortcut for verifying if it's NaN
          n = 0;
        } else if (n !== 0 && n !== Infinity && n !== -Infinity) {
          n = (n > 0 || -1) * Math.floor(Math.abs(n));
        }
      }
      if (n >= len) {
        return -1;
      }
      var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0);
      for (; k < len; k++) {
        if (k in t && t[k] === searchElement) {
          return k;
        }
      }
      return -1;
    };
  }

  /**
   * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
   */
  if(!Array.isArray) {
    Array.isArray = function (vArg) {
      return Object.prototype.toString.call(vArg) === "[object Array]";
    };
  }

  /**
   * Download and execute the script at the given url. Fire callback when done.
   *
   * @param url
   * @param callback
   */
  play.getScript = function(url, callback) {
    Utils.assert(url, "getScript called with null url");

    var script = document.createElement("script");
    script.src = url;
    play.executeScript(script, callback);
  };

  /**
   * Execute the JavaScript in script, which should be a DOM script node. Fire callback when done.
   *
   * Stolen from: https://github.com/jquery/jquery/blob/master/src/ajax/script.js
   *
   * @param script
   * @param callback
   */
  play.executeScript = function(script, callback) {
    Utils.assert(play.isClient, "executeScript should only be used for client-side rendering!");
    Utils.assert(script, "executeScript called with null script");

    var head = document.head || document.getElementsByTagName("head")[0] || document.documentElement;

    script.async = "async";

    var cleanup = function() {
      if (head && script.parentNode) {
        head.removeChild(script);
      }
      script = undefined;
    };

    if (script.src) {
      script.onload = script.onreadystatechange = function(_, isAbort) {
        if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState)) {
          script.onload = script.onreadystatechange = null;
          cleanup();
          if (!isAbort && callback) {
            callback();
          }
        }
      };
    }

    /* We have to keep a reference to the inserted HTMLScriptElement from insertBefore return value as
     IE may trigger the onload/onreadystatechange callback before returning control if the script source
     file is already cached by the browser, and our cleanup function declared above would render the
     script variable undefined. */
    script = head.insertBefore(script, head.firstChild);
    if (typeof(script) !== 'undefined' && !script.src) {
      cleanup();
      if (callback) {
        callback();
      }
    }
  };

  /**
   * Given a path like "A.B.C", safely traverse obj and return the value obj["A"]["B"]["C"]. If at any point an
   * intermediate value is null (e.g. obj["A"]["B"] is null before getting to ["C"]), return null, unless required is
   * set to true, in which case, throw an exception.
   *
   * @param obj
   * @param path
   * @param required
   * @return {*}
   */
  play.traverseObject = function(obj, path, required) {
    Utils.assert(path, "traverseObject called with null path");
    Utils.assert(obj || !required, "traverseObject called with a null object, but required is set to true");

    var parts = path.split(".");

    for (var i = 0; i < parts.length; i++) {
      obj = obj ? obj[parts[i]] : obj;
      if (!obj) {
        Utils.assert(!required, "traverseObject could not find required path " + path);
        return null;
      }
    }

    return obj;
  };

  /**
   * Utility function to print a log. Can be used in both normal browsers and Rhino envrionment. Noop if not in either
   * environment.
   *
   * @param msg
   */
  play.log = function(msg) {
    if (root.console && root.console.log) {
      // Normal browsers
      root.console.log(msg);
    } else if (root.java && root.java.lang && root.java.lang.System && root.java.lang.System.out && root.java.lang.System.out.println) {
      // Rhino: http://stackoverflow.com/questions/12399462/rhino-print-function
      root.java.lang.System.out.println(msg);
    }
  };

})(play, t8, this);