text: /*
   * typeahead.js
   * https://github.com/twitter/typeahead.js
   * Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
   */
  
  (function(root) {
    'use strict';
  
    var old, keys;
  
    old = root.Bloodhound;
    keys = { data: 'data', protocol: 'protocol', thumbprint: 'thumbprint' };
  
    // add Bloodhoud to global context
    root.Bloodhound = Bloodhound;
  
    // constructor
    // -----------
  
    function Bloodhound(o) {
      if (!o || (!o.local && !o.prefetch && !o.remote)) {
        $.error('one of local, prefetch, or remote is required');
      }
  
      this.limit = o.limit || 5;
      this.sorter = getSorter(o.sorter);
      this.dupDetector = o.dupDetector || ignoreDuplicates;
  
      this.local = oParser.local(o);
      this.prefetch = oParser.prefetch(o);
      this.remote = oParser.remote(o);
  
      this.cacheKey = this.prefetch ?
        (this.prefetch.cacheKey || this.prefetch.url) : null;
  
      // the backing data structure used for fast pattern matching
      this.index = new SearchIndex({
        datumTokenizer: o.datumTokenizer,
        queryTokenizer: o.queryTokenizer
      });
  
      // only initialize storage if there's a cacheKey otherwise
      // loading from storage on subsequent page loads is impossible
      this.storage = this.cacheKey ? new PersistentStorage(this.cacheKey) : null;
    }
  
    // static methods
    // --------------
  
    Bloodhound.noConflict = function noConflict() {
      root.Bloodhound = old;
      return Bloodhound;
    };
  
    Bloodhound.tokenizers = tokenizers;
  
    // instance methods
    // ----------------
  
    _.mixin(Bloodhound.prototype, {
  
      // ### private
  
      _loadPrefetch: function loadPrefetch(o) {
        var that = this, serialized, deferred;
  
        if (serialized = this._readFromStorage(o.thumbprint)) {
          this.index.bootstrap(serialized);
          deferred = $.Deferred().resolve();
        }
  
        else {
          deferred = $.ajax(o.url, o.ajax).done(handlePrefetchResponse);
        }
  
        return deferred;
  
        function handlePrefetchResponse(resp) {
          // clear to mirror the behavior of bootstrapping
          that.clear();
          that.add(o.filter ? o.filter(resp) : resp);
  
          that._saveToStorage(that.index.serialize(), o.thumbprint, o.ttl);
        }
      },
  
      _getFromRemote: function getFromRemote(query, cb) {
        var that = this, url, uriEncodedQuery;
  
        if (!this.transport) { return; }
  
        query = query || '';
        uriEncodedQuery = encodeURIComponent(query);
  
        url = this.remote.replace ?
          this.remote.replace(this.remote.url, query) :
          this.remote.url.replace(this.remote.wildcard, uriEncodedQuery);
  
        return this.transport.get(url, this.remote.ajax, handleRemoteResponse);
  
        function handleRemoteResponse(err, resp) {
          err ? cb([]) : cb(that.remote.filter ? that.remote.filter(resp) : resp);
        }
      },
  
      _cancelLastRemoteRequest: function cancelLastRemoteRequest() {
        // #149: prevents outdated rate-limited requests from being sent
        this.transport && this.transport.cancel();
      },
  
      _saveToStorage: function saveToStorage(data, thumbprint, ttl) {
        if (this.storage) {
          this.storage.set(keys.data, data, ttl);
          this.storage.set(keys.protocol, location.protocol, ttl);
          this.storage.set(keys.thumbprint, thumbprint, ttl);
        }
      },
  
      _readFromStorage: function readFromStorage(thumbprint) {
        var stored = {}, isExpired;
  
        if (this.storage) {
          stored.data = this.storage.get(keys.data);
          stored.protocol = this.storage.get(keys.protocol);
          stored.thumbprint = this.storage.get(keys.thumbprint);
        }
        // the stored data is considered expired if the thumbprints
        // don't match or if the protocol it was originally stored under
        // has changed
        isExpired = stored.thumbprint !== thumbprint ||
          stored.protocol !== location.protocol;
  
        return stored.data && !isExpired ? stored.data : null;
      },
  
      _initialize: function initialize() {
        var that = this, local = this.local, deferred;
  
        deferred = this.prefetch ?
          this._loadPrefetch(this.prefetch) : $.Deferred().resolve();
  
        // make sure local is added to the index after prefetch
        local && deferred.done(addLocalToIndex);
  
        this.transport = this.remote ? new Transport(this.remote) : null;
  
        return (this.initPromise = deferred.promise());
  
        function addLocalToIndex() {
          // local can be a function that returns an array of datums
          that.add(_.isFunction(local) ? local() : local);
        }
      },
  
      // ### public
  
      initialize: function initialize(force) {
        return !this.initPromise || force ? this._initialize() : this.initPromise;
      },
  
      add: function add(data) {
        this.index.add(data);
      },
  
      get: function get(query, cb) {
        var that = this, matches = [], cacheHit = false;
  
        matches = this.index.get(query);
        matches = this.sorter(matches).slice(0, this.limit);
  
        matches.length < this.limit ?
          (cacheHit = this._getFromRemote(query, returnRemoteMatches)) :
          this._cancelLastRemoteRequest();
  
        // if a cache hit occurred, skip rendering local matches
        // because the rendering of local/remote matches is already
        // in the event loop
        if (!cacheHit) {
          // only render if there are some local suggestions or we're
          // going to the network to backfill
          (matches.length > 0 || !this.transport) && cb && cb(matches);
        }
  
        function returnRemoteMatches(remoteMatches) {
          var matchesWithBackfill = matches.slice(0);
  
          _.each(remoteMatches, function(remoteMatch) {
            var isDuplicate;
  
            // checks for duplicates
            isDuplicate = _.some(matchesWithBackfill, function(match) {
              return that.dupDetector(remoteMatch, match);
            });
  
            !isDuplicate && matchesWithBackfill.push(remoteMatch);
  
            // if we're at the limit, we no longer need to process
            // the remote results and can break out of the each loop
            return matchesWithBackfill.length < that.limit;
          });
          cb && cb(that.sorter(matchesWithBackfill));
        }
      },
  
      clear: function clear() {
        this.index.reset();
      },
  
      clearPrefetchCache: function clearPrefetchCache() {
        this.storage && this.storage.clear();
      },
  
      clearRemoteCache: function clearRemoteCache() {
        this.transport && Transport.resetCache();
      },
  
      ttAdapter: function ttAdapter() { return _.bind(this.get, this); }
    });
  
    return Bloodhound;
  
    // helper functions
    // ----------------
  
    function getSorter(sortFn) {
      return _.isFunction(sortFn) ? sort : noSort;
  
      function sort(array) { return array.sort(sortFn); }
      function noSort(array) { return array; }
    }
  
    function ignoreDuplicates() { return false; }
  })(this);
  