diff options
| author | Matias Niemelä | 2013-05-09 21:53:07 -0400 | 
|---|---|---|
| committer | Igor Minar | 2013-05-16 16:17:46 -0700 | 
| commit | 3a49b7eec4836ec9dc1588e6cedda942755dc7bf (patch) | |
| tree | e6daf1614c8d864858cafaa0a36751488c874e05 /docs/src | |
| parent | 0401a7f598ef9a36ffe1f217e1a98961046fa551 (diff) | |
| download | angular.js-3a49b7eec4836ec9dc1588e6cedda942755dc7bf.tar.bz2 | |
feat(ngdocs): Add FullText search to replace Google search in docs
Diffstat (limited to 'docs/src')
| -rw-r--r-- | docs/src/ignore.words | 702 | ||||
| -rw-r--r-- | docs/src/templates/css/docs.css | 35 | ||||
| -rw-r--r-- | docs/src/templates/index.html | 18 | ||||
| -rw-r--r-- | docs/src/templates/js/docs.js | 116 | ||||
| -rw-r--r-- | docs/src/templates/js/lunr.js | 1560 | 
5 files changed, 2428 insertions, 3 deletions
| diff --git a/docs/src/ignore.words b/docs/src/ignore.words index e69de29b..d3af04b0 100644 --- a/docs/src/ignore.words +++ b/docs/src/ignore.words @@ -0,0 +1,702 @@ +a +able +about +above +abst +accordance +according +accordingly +across +act +actually +added +adj +adopted +affected +affecting +affects +after +afterwards +again +against +ah +all +almost +alone +along +already +also +although +always +am +among +amongst +an +and +announce +another +any +anybody +anyhow +anymore +anyone +anything +anyway +anyways +anywhere +apparently +approximately +are +aren +arent +arise +around +as +aside +ask +asking +at +auth +available +away +awfully +b +back +be +became +because +become +becomes +becoming +been +before +beforehand +begin +beginning +beginnings +begins +behind +being +believe +below +beside +besides +between +beyond +biol +both +brief +briefly +but +by +c +ca +came +can +cannot +can't +cant +cause +causes +certain +certainly +co +com +come +comes +contain +containing +contains +could +couldnt +d +date +did +didn't +didnt +different +do +does +doesn't +doesnt +doing +done +don't +dont +down +downwards +due +during +e +each +ed +edu +effect +eg +eight +eighty +either +else +elsewhere +end +ending +enough +especially +et +et-al +etc +even +ever +every +everybody +everyone +everything +everywhere +ex +except +f +far +few +ff +fifth +first +five +fix +followed +following +follows +for +former +formerly +forth +found +four +from +further +furthermore +g +gave +get +gets +getting +give +given +gives +giving +go +goes +gone +got +gotten +h +had +happens +hardly +has +hasn't +hasnt +have +haven't +havent +having +he +hed +hence +her +here +hereafter +hereby +herein +heres +hereupon +hers +herself +hes +hi +hid +him +himself +his +hither +home +how +howbeit +however +hundred +i +id +ie +if +i'll +ill +im +immediate +immediately +importance +important +in +inc +indeed +index +information +instead +into +invention +inward +is +isn't +isnt +it +itd +it'll +itll +its +itself +i've +ive +j +just +k +keep +keeps +kept +keys +kg +km +know +known +knows +l +largely +last +lately +later +latter +latterly +least +less +lest +let +lets +like +liked +likely +line +little +'ll +'ll +look +looking +looks +ltd +m +made +mainly +make +makes +many +may +maybe +me +mean +means +meantime +meanwhile +merely +mg +might +million +miss +ml +more +moreover +most +mostly +mr +mrs +much +mug +must +my +myself +n +na +name +namely +nay +nd +near +nearly +necessarily +necessary +need +needs +neither +never +nevertheless +new +next +nine +ninety +no +nobody +non +none +nonetheless +noone +nor +normally +nos +not +noted +nothing +now +nowhere +o +obtain +obtained +obviously +of +off +often +oh +ok +okay +old +omitted +on +once +one +ones +only +onto +or +ord +other +others +otherwise +ought +our +ours +ourselves +out +outside +over +overall +owing +own +p +page +pages +part +particular +particularly +past +per +perhaps +placed +please +plus +poorly +possible +possibly +potentially +pp +predominantly +present +previously +primarily +probably +promptly +proud +provides +put +q +que +quickly +quite +qv +r +ran +rather +rd +re +readily +really +recent +recently +ref +refs +regarding +regardless +regards +related +relatively +research +respectively +resulted +resulting +results +right +run +s +said +same +saw +say +saying +says +sec +section +see +seeing +seem +seemed +seeming +seems +seen +self +selves +sent +seven +several +shall +she +shed +she'll +shell +shes +should +shouldn't +shouldnt +show +showed +shown +showns +shows +significant +significantly +similar +similarly +since +six +slightly +so +some +somebody +somehow +someone +somethan +something +sometime +sometimes +somewhat +somewhere +soon +sorry +specifically +specified +specify +specifying +state +states +still +stop +strongly +sub +substantially +successfully +such +sufficiently +suggest +sup +sure +t +take +taken +taking +tell +tends +th +than +thank +thanks +thanx +that +that'll +thatll +thats +that've +thatve +the +their +theirs +them +themselves +then +thence +there +thereafter +thereby +thered +therefore +therein +there'll +therell +thereof +therere +theres +thereto +thereupon +there've +thereve +these +they +theyd +they'll +theyll +theyre +they've +theyve +think +this +those +thou +though +thoughh +thousand +throug +through +throughout +thru +thus +til +tip +to +together +too +took +toward +towards +tried +tries +truly +try +trying +ts +twice +two +u +un +under +unfortunately +unless +unlike +unlikely +until +unto +up +upon +ups +us +use +used +useful +usefully +usefulness +uses +using +usually +v +value +various +'ve +'ve +very +via +viz +vol +vols +vs +w +want +wants +was +wasn't +wasnt +way +we +wed +welcome +we'll +well +went +were +weren't +werent +we've +weve +what +whatever +what'll +whatll +whats +when +whence +whenever +where +whereafter +whereas +whereby +wherein +wheres +whereupon +wherever +whether +which +while +whim +whither +who +whod +whoever +whole +who'll +wholl +whom +whomever +whos +whose +why +widely +will +willing +wish +with +within +without +won't +wont +words +world +would +wouldn't +wouldnt +www +x +y +yes +yet +you +youd +you'll +youll +your +youre +yours +yourself +yourselves +you've +youve +z +zero diff --git a/docs/src/templates/css/docs.css b/docs/src/templates/css/docs.css index c15172ca..dd0ad5b7 100644 --- a/docs/src/templates/css/docs.css +++ b/docs/src/templates/css/docs.css @@ -259,4 +259,39 @@ ul.events > li > h3 {  .syntax-links + pre {    border-top-left-radius:0;    border-top-right-radius:0; + +.search-results { +  clear:both; +  display:table; +  width:100%; +} + +.search-results > .search-group { +  vertical-align:top; +  padding:10px 0; +  display:table-cell; +} + +.search-group.cols-1 { width:100%; } +.search-group.cols-2 { width:50%; } +.search-group.cols-3 { width:33%; } +.search-group.cols-4 { width:25%; } + +.search-close { +  z-index:1029; +  position:absolute; +  bottom:-25px; +  left:80%; +  text-align:center; +  line-height:50px; +  width:50px; +  font-size:2em; +  background:#222222; +  border-radius:15px; +} + +.search-close span { +  text-decoration:none; +  position:relative; +  z-index:1031;  } diff --git a/docs/src/templates/index.html b/docs/src/templates/index.html index 53f23939..3d5fd4c5 100644 --- a/docs/src/templates/index.html +++ b/docs/src/templates/index.html @@ -43,6 +43,7 @@        addTag('script', {src: path('angular-mobile.js') }, sync);        addTag('script', {src: path('angular-bootstrap.js') }, sync);        addTag('script', {src: path('angular-bootstrap-prettify.js') }, sync); +      addTag('script', {src: 'js/lunr.js' }, sync);        addTag('script', {src: 'js/docs.js'}, sync);        addTag('script', {src: 'docs-keywords.js'}, sync); @@ -112,7 +113,7 @@  </head>  <body> -  <header class="header"> +  <header class="header" ng-controller="DocsNavigationCtrl">      <div class="navbar navbar-fixed-top">        <div class="navbar-inner">          <div class="container"> @@ -166,10 +167,21 @@              </li>              <li class="divider-vertical"></li>            </ul> -          <form class="navbar-search pull-right" method="GET" action="https://www.google.com/search"> -            <input type="text" name="as_q" class="search-query" placeholder="Search"> +          <form class="navbar-search pull-right" ng-submit="submit()"> +            <input type="text" name="as_q" class="search-query" placeholder="Search" ng-change="search(q)" ng-model="q" autocomplete="off">              <input type="hidden" name="as_sitesearch" value="angularjs.org">            </form> +          <div ng-show="hasResults" class="search-results"> +            <a href="" ng-click="hideResults()" class="search-close"> +              <span class="icon-remove-sign"></span> +            </a> +            <div ng-repeat="(key, value) in results" class="search-group" ng-class="colClassName"> +              <h4>{{ key }}</h4> +              <div ng-repeat="item in value" class="search-result"> +                <a ng-click="hideResults()" href="{{ item.url }}">{{ item.shortName }}</a> +              </div> +            </div> +          </div>          </div>        </div>      </div> diff --git a/docs/src/templates/js/docs.js b/docs/src/templates/js/docs.js index a43aa601..cbb0a452 100644 --- a/docs/src/templates/js/docs.js +++ b/docs/src/templates/js/docs.js @@ -4,6 +4,122 @@ var docsApp = {    serviceFactory: {}  }; +docsApp.controller.DocsNavigationCtrl = ['$scope', 'fullTextSearch', '$location', function($scope, fullTextSearch, $location) { +  fullTextSearch.init(); +  $scope.search = function(q) { +    fullTextSearch.search(q, function(results) { +      if(q && q.length >= 4) { +        $scope.results = results; +        var totalSections = 0; +        for(var i in results) { +          ++totalSections; +        } +        if(totalSections > 0) { +          $scope.colClassName = 'cols-' + totalSections; +          $scope.hasResults = true; +        } +        else { +          $scope.hasResults = false; +        } +      } +      else { +        $scope.hasResults = false; +      } +      if(!$scope.$$phase) $scope.$apply(); +    }); +  }; +  $scope.submit = function() { +    var result; +    for(var i in $scope.results) { +      result = $scope.results[i][0]; +      if(result) { +        break; +      } +    } +    if(result) { +      $location.path(result.url); +      $scope.hideResults(); +    } +  }; +  $scope.hideResults = function() { +    $scope.hasResults = false; +    $scope.q = ''; +  }; +}]; + +docsApp.serviceFactory.fullTextSearch = ['$q', '$rootScope', function($q, $rootScope) { +  return { +    dbName : 'docs', +    indexName : 'docsindex', + +    init : function(onReady) { +      this.init = function() {}; + +      var self = this; +      this.deferReady = $q.defer(); +      this.readyPromise = this.deferReady.promise; + +      this.engine = lunr(function () { +        this.ref('id'); +        this.field('title', {boost: 50}); +        this.field('description', { boost : 20 }); +      }); +      this.prepare(); +      this.onReady(); +    }, +    onReady : function() { +      this.ready = true; +      var self = this; +      self.deferReady.resolve();  +      if(!$rootScope.$$phase) { +        $rootScope.$apply(); +      } +    }, +    whenReady : function(fn) { +      if(this.ready) { +        fn(); +      } +      else { +        this.init(); +        this.readyPromise.then(fn); +      } +    }, +    prepare : function(injector, callback) { +      for(var i=0;i<NG_PAGES.length;i++) { +        var page = NG_PAGES[i]; +        var title = page.shortName; +        if(title.charAt(0) == 'n' && title.charAt(1) == 'g') { +          title = title + ' ' + title.substr(2); +        } +        this.engine.add({ +          id: i, +          title: title, +          description: page.keywords +        }); +      } +    }, +    search : function(q, onReady) { +      var self = this; +      this.whenReady(function() { +        var data = []; +        var results = self.engine.search(q); +        var groups = {}; +        angular.forEach(results, function(result) { +          var item = NG_PAGES[result.ref]; +          var section = item.section; +          if(section == 'cookbook') { +            section = 'tutorial'; +          } +          groups[section] = groups[section] || []; +          if(groups[section].length < 15) { +            groups[section].push(item); +          } +        }) +        onReady(groups); +      }) +    } +  }; +}];  docsApp.directive.focused = function($timeout) {    return function(scope, element, attrs) { diff --git a/docs/src/templates/js/lunr.js b/docs/src/templates/js/lunr.js new file mode 100644 index 00000000..6bbd38b3 --- /dev/null +++ b/docs/src/templates/js/lunr.js @@ -0,0 +1,1560 @@ +/** + * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 0.3.1 + * Copyright (C) 2013 Oliver Nightingale + * MIT Licensed + * @license + */ + +/** + * Convenience function for instantiating a new lunr index and configuring it + * with the default pipeline functions and the passed config function. + * + * When using this convenience function a new index will be created with the + * following functions already in the pipeline: + * + * lunr.StopWordFilter - filters out any stop words before they enter the + * index + * + * lunr.stemmer - stems the tokens before entering the index. + * + * Example: + * + *     var idx = lunr(function () { + *       this.field('title', 10) + *       this.field('tags', 100) + *       this.field('body') + *        + *       this.ref('cid') + *        + *       this.pipeline.add(function () { + *         // some custom pipeline function + *       }) + *        + *     }) + * + * @param {Function} config A function that will be called with the new instance + * of the lunr.Index as both its context and first parameter. It can be used to + * customize the instance of new lunr.Index. + * @namespace + * @module + * @returns {lunr.Index} + * + */ +var lunr = function (config) { +  var idx = new lunr.Index + +  idx.pipeline.add(lunr.stopWordFilter, lunr.stemmer) + +  if (config) config.call(idx, idx) + +  return idx +} + +lunr.version = "0.3.1" + +if (typeof module !== 'undefined') { +  module.exports = lunr +} +/*! + * lunr.tokenizer + * Copyright (C) 2013 Oliver Nightingale + */ + +/** + * A function for splitting a string into tokens ready to be inserted into + * the search index. + * + * @module + * @param {String} str The string to convert into tokens + * @returns {Array} + */ +lunr.tokenizer = function (str) { +  if (Array.isArray(str)) return str + +  var str = str.replace(/^\s+/, '') + +  for (var i = str.length - 1; i >= 0; i--) { +    if (/\S/.test(str.charAt(i))) { +      str = str.substring(0, i + 1) +      break +    } +  } + +  return str +    .split(/\s+/) +    .map(function (token) { +      return token.replace(/^\W+/, '').replace(/\W+$/, '').toLowerCase() +    }) +} +/*! + * lunr.Pipeline + * Copyright (C) 2013 Oliver Nightingale + */ + +/** + * lunr.Pipelines maintain an ordered list of functions to be applied to all + * tokens in documents entering the search index and queries being ran against + * the index. + * + * An instance of lunr.Index created with the lunr shortcut will contain a + * pipeline with a stop word filter and an English language stemmer. Extra + * functions can be added before or after either of these functions or these + * default functions can be removed. + * + * When run the pipeline will call each function in turn, passing a token, the + * index of that token in the original list of all tokens and finally a list of + * all the original tokens. + * + * The output of functions in the pipeline will be passed to the next function + * in the pipeline. To exclude a token from entering the index the function + * should return undefined, the rest of the pipeline will not be called with + * this token. + * + * For serialisation of pipelines to work, all functions used in an instance of + * a pipeline should be registered with lunr.Pipeline. Registered functions can + * then be loaded. If trying to load a serialised pipeline that uses functions + * that are not registered an error will be thrown. + * + * If not planning on serialising the pipeline then registering pipeline functions + * is not necessary. + * + * @constructor + */ +lunr.Pipeline = function () { +  this._stack = [] +} + +lunr.Pipeline.registeredFunctions = {} + +/** + * Register a function with the pipeline. + * + * Functions that are used in the pipeline should be registered if the pipeline + * needs to be serialised, or a serialised pipeline needs to be loaded. + * + * Registering a function does not add it to a pipeline, functions must still be + * added to instances of the pipeline for them to be used when running a pipeline. + * + * @param {Function} fn The function to check for. + * @param {String} label The label to register this function with + * @memberOf Pipeline + */ +lunr.Pipeline.registerFunction = function (fn, label) { +  if (console && console.warn && (label in this.registeredFunctions)) { +    console.warn('Overwriting existing registered function: ' + label) +  } + +  fn.label = label +  lunr.Pipeline.registeredFunctions[fn.label] = fn +} + +/** + * Warns if the function is not registered as a Pipeline function. + * + * @param {Function} fn The function to check for. + * @private + * @memberOf Pipeline + */ +lunr.Pipeline.warnIfFunctionNotRegistered = function (fn) { +  var isRegistered = fn.label && (fn.label in this.registeredFunctions) + +  if (!isRegistered && console && console.warn) { +    console.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\n', fn) +  } +} + +/** + * Loads a previously serialised pipeline. + * + * All functions to be loaded must already be registered with lunr.Pipeline. + * If any function from the serialised data has not been registered then an + * error will be thrown. + * + * @param {Object} serialised The serialised pipeline to load. + * @returns {lunr.Pipeline} + * @memberOf Pipeline + */ +lunr.Pipeline.load = function (serialised) { +  var pipeline = new lunr.Pipeline + +  serialised.forEach(function (fnName) { +    var fn = lunr.Pipeline.registeredFunctions[fnName] + +    if (fn) { +      pipeline.add(fn) +    } else { +      throw new Error ('Cannot load un-registered function: ' + fnName) +    } +  }) + +  return pipeline +} + +/** + * Adds new functions to the end of the pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {Function} functions Any number of functions to add to the pipeline. + * @memberOf Pipeline + */ +lunr.Pipeline.prototype.add = function () { +  var fns = Array.prototype.slice.call(arguments) + +  fns.forEach(function (fn) { +    lunr.Pipeline.warnIfFunctionNotRegistered(fn) +    this._stack.push(fn) +  }, this) +} + +/** + * Adds a single function after a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {Function} existingFn A function that already exists in the pipeline. + * @param {Function} newFn The new function to add to the pipeline. + * @memberOf Pipeline + */ +lunr.Pipeline.prototype.after = function (existingFn, newFn) { +  lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + +  var pos = this._stack.indexOf(existingFn) + 1 +  this._stack.splice(pos, 0, newFn) +} + +/** + * Adds a single function before a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {Function} existingFn A function that already exists in the pipeline. + * @param {Function} newFn The new function to add to the pipeline. + * @memberOf Pipeline + */ +lunr.Pipeline.prototype.before = function (existingFn, newFn) { +  lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + +  var pos = this._stack.indexOf(existingFn) +  this._stack.splice(pos, 0, newFn) +} + +/** + * Removes a function from the pipeline. + * + * @param {Function} fn The function to remove from the pipeline. + * @memberOf Pipeline + */ +lunr.Pipeline.prototype.remove = function (fn) { +  var pos = this._stack.indexOf(fn) +  this._stack.splice(pos, 1) +} + +/** + * Runs the current list of functions that make up the pipeline against the + * passed tokens. + * + * @param {Array} tokens The tokens to run through the pipeline. + * @returns {Array} + * @memberOf Pipeline + */ +lunr.Pipeline.prototype.run = function (tokens) { +  var out = [], +      tokenLength = tokens.length, +      stackLength = this._stack.length + +  for (var i = 0; i < tokenLength; i++) { +    var token = tokens[i] + +    for (var j = 0; j < stackLength; j++) { +      token = this._stack[j](token, i, tokens) +      if (token === void 0) break +    }; + +    if (token !== void 0) out.push(token) +  }; + +  return out +} + +/** + * Returns a representation of the pipeline ready for serialisation. + * + * Logs a warning if the function has not been registered. + * + * @returns {Array} + * @memberOf Pipeline + */ +lunr.Pipeline.prototype.toJSON = function () { +  return this._stack.map(function (fn) { +    lunr.Pipeline.warnIfFunctionNotRegistered(fn) + +    return fn.label +  }) +} +/*! + * lunr.Vector + * Copyright (C) 2013 Oliver Nightingale + */ + +/** + * lunr.Vectors wrap arrays and add vector related operations for the array + * elements. + * + * @constructor + * @param {Array} elements Elements that make up the vector. + */ +lunr.Vector = function (elements) { +  this.elements = elements + +  for (var i = 0; i < elements.length; i++) { +    if (!(i in this.elements)) this.elements[i] = 0 +  } +} + +/** + * Calculates the magnitude of this vector. + * + * @returns {Number} + * @memberOf Vector + */ +lunr.Vector.prototype.magnitude = function () { +  if (this._magnitude) return this._magnitude + +  var sumOfSquares = 0, +      elems = this.elements, +      len = elems.length, +      el + +  for (var i = 0; i < len; i++) { +    el = elems[i] +    sumOfSquares += el * el +  }; + +  return this._magnitude = Math.sqrt(sumOfSquares) +} + +/** + * Calculates the dot product of this vector and another vector. + * + * @param {lunr.Vector} otherVector The vector to compute the dot product with. + * @returns {Number} + * @memberOf Vector + */ +lunr.Vector.prototype.dot = function (otherVector) { +  var elem1 = this.elements, +      elem2 = otherVector.elements, +      length = elem1.length, +      dotProduct = 0 + +  for (var i = 0; i < length; i++) { +    dotProduct += elem1[i] * elem2[i] +  }; + +  return dotProduct +} + +/** + * Calculates the cosine similarity between this vector and another + * vector. + * + * @param {lunr.Vector} otherVector The other vector to calculate the + * similarity with. + * @returns {Number} + * @memberOf Vector + */ +lunr.Vector.prototype.similarity = function (otherVector) { +  return this.dot(otherVector) / (this.magnitude() * otherVector.magnitude()) +} + +/** + * Converts this vector back into an array. + * + * @returns {Array} + * @memberOf Vector + */ +lunr.Vector.prototype.toArray = function () { +  return this.elements +} +/*! + * lunr.SortedSet + * Copyright (C) 2013 Oliver Nightingale + */ + +/** + * lunr.SortedSets are used to maintain an array of uniq values in a sorted + * order. + * + * @constructor + */ +lunr.SortedSet = function () { +  this.length = 0 +  this.elements = [] +} + +/** + * Loads a previously serialised sorted set. + * + * @param {Array} serialisedData The serialised set to load. + * @returns {lunr.SortedSet} + * @memberOf SortedSet + */ +lunr.SortedSet.load = function (serialisedData) { +  var set = new this + +  set.elements = serialisedData +  set.length = serialisedData.length + +  return set +} + +/** + * Inserts new items into the set in the correct position to maintain the + * order. + * + * @param {Object} The objects to add to this set. + * @memberOf SortedSet + */ +lunr.SortedSet.prototype.add = function () { +  Array.prototype.slice.call(arguments).forEach(function (element) { +    if (~this.indexOf(element)) return +    this.elements.splice(this.locationFor(element), 0, element) +  }, this) + +  this.length = this.elements.length +} + +/** + * Converts this sorted set into an array. + * + * @returns {Array} + * @memberOf SortedSet + */ +lunr.SortedSet.prototype.toArray = function () { +  return this.elements.slice() +} + +/** + * Creates a new array with the results of calling a provided function on every + * element in this sorted set. + * + * Delegates to Array.prototype.map and has the same signature. + * + * @param {Function} fn The function that is called on each element of the + * set. + * @param {Object} ctx An optional object that can be used as the context + * for the function fn. + * @returns {Array} + * @memberOf SortedSet + */ +lunr.SortedSet.prototype.map = function (fn, ctx) { +  return this.elements.map(fn, ctx) +} + +/** + * Executes a provided function once per sorted set element. + * + * Delegates to Array.prototype.forEach and has the same signature. + * + * @param {Function} fn The function that is called on each element of the + * set. + * @param {Object} ctx An optional object that can be used as the context + * @memberOf SortedSet + * for the function fn. + */ +lunr.SortedSet.prototype.forEach = function (fn, ctx) { +  return this.elements.forEach(fn, ctx) +} + +/** + * Returns the index at which a given element can be found in the + * sorted set, or -1 if it is not present. + * + * @param {Object} elem The object to locate in the sorted set. + * @param {Number} start An optional index at which to start searching from + * within the set. + * @param {Number} end An optional index at which to stop search from within + * the set. + * @returns {Number} + * @memberOf SortedSet + */ +lunr.SortedSet.prototype.indexOf = function (elem, start, end) { +  var start = start || 0, +      end = end || this.elements.length, +      sectionLength = end - start, +      pivot = start + Math.floor(sectionLength / 2), +      pivotElem = this.elements[pivot] + +  if (sectionLength <= 1) { +    if (pivotElem === elem) { +      return pivot +    } else { +      return -1 +    } +  } + +  if (pivotElem < elem) return this.indexOf(elem, pivot, end) +  if (pivotElem > elem) return this.indexOf(elem, start, pivot) +  if (pivotElem === elem) return pivot +} + +/** + * Returns the position within the sorted set that an element should be + * inserted at to maintain the current order of the set. + * + * This function assumes that the element to search for does not already exist + * in the sorted set. + * + * @param {Object} elem The elem to find the position for in the set + * @param {Number} start An optional index at which to start searching from + * within the set. + * @param {Number} end An optional index at which to stop search from within + * the set. + * @returns {Number} + * @memberOf SortedSet + */ +lunr.SortedSet.prototype.locationFor = function (elem, start, end) { +  var start = start || 0, +      end = end || this.elements.length, +      sectionLength = end - start, +      pivot = start + Math.floor(sectionLength / 2), +      pivotElem = this.elements[pivot] + +  if (sectionLength <= 1) { +    if (pivotElem > elem) return pivot +    if (pivotElem < elem) return pivot + 1 +  } + +  if (pivotElem < elem) return this.locationFor(elem, pivot, end) +  if (pivotElem > elem) return this.locationFor(elem, start, pivot) +} + +/** + * Creates a new lunr.SortedSet that contains the elements in the intersection + * of this set and the passed set. + * + * @param {lunr.SortedSet} otherSet The set to intersect with this set. + * @returns {lunr.SortedSet} + * @memberOf SortedSet + */ +lunr.SortedSet.prototype.intersect = function (otherSet) { +  var intersectSet = new lunr.SortedSet, +      i = 0, j = 0, +      a_len = this.length, b_len = otherSet.length, +      a = this.elements, b = otherSet.elements + +  while (true) { +    if (i > a_len - 1 || j > b_len - 1) break + +    if (a[i] === b[j]) { +      intersectSet.add(a[i]) +      i++, j++ +      continue +    } + +    if (a[i] < b[j]) { +      i++ +      continue +    } + +    if (a[i] > b[j]) { +      j++ +      continue +    } +  }; + +  return intersectSet +} + +/** + * Makes a copy of this set + * + * @returns {lunr.SortedSet} + * @memberOf SortedSet + */ +lunr.SortedSet.prototype.clone = function () { +  var clone = new lunr.SortedSet + +  clone.elements = this.toArray() +  clone.length = clone.elements.length + +  return clone +} + +/** + * Creates a new lunr.SortedSet that contains the elements in the union + * of this set and the passed set. + * + * @param {lunr.SortedSet} otherSet The set to union with this set. + * @returns {lunr.SortedSet} + * @memberOf SortedSet + */ +lunr.SortedSet.prototype.union = function (otherSet) { +  var longSet, shortSet, unionSet + +  if (this.length >= otherSet.length) { +    longSet = this, shortSet = otherSet +  } else { +    longSet = otherSet, shortSet = this +  } + +  unionSet = longSet.clone() + +  unionSet.add.apply(unionSet, shortSet.toArray()) + +  return unionSet +} + +/** + * Returns a representation of the sorted set ready for serialisation. + * + * @returns {Array} + * @memberOf SortedSet + */ +lunr.SortedSet.prototype.toJSON = function () { +  return this.toArray() +} +/*! + * lunr.Index + * Copyright (C) 2013 Oliver Nightingale + */ + +/** + * lunr.Index is object that manages a search index.  It contains the indexes + * and stores all the tokens and document lookups.  It also provides the main + * user facing API for the library. + * + * @constructor + */ +lunr.Index = function () { +  this._fields = [] +  this._ref = 'id' +  this.pipeline = new lunr.Pipeline +  this.documentStore = new lunr.Store +  this.tokenStore = new lunr.TokenStore +  this.corpusTokens = new lunr.SortedSet +} + + +/** + * Loads a previously serialised index. + * + * Issues a warning if the index being imported was serialised + * by a different version of lunr. + * + * @param {Object} serialisedData The serialised set to load. + * @returns {lunr.Index} + * @memberOf Index + */ +lunr.Index.load = function (serialisedData) { +  if (serialisedData.version !== lunr.version && console && console.warn) { +    console.warn('version mismatch: current ' + lunr.version + ' importing ' + serialisedData.version) +  } + +  var idx = new this + +  idx._fields = serialisedData.fields +  idx._ref = serialisedData.ref + +  idx.documentStore = lunr.Store.load(serialisedData.documentStore) +  idx.tokenStore = lunr.TokenStore.load(serialisedData.tokenStore) +  idx.corpusTokens = lunr.SortedSet.load(serialisedData.corpusTokens) +  idx.pipeline = lunr.Pipeline.load(serialisedData.pipeline) + +  return idx +} + +/** + * Adds a field to the list of fields that will be searchable within documents + * in the index. + * + * An optional boost param can be passed to affect how much tokens in this field + * rank in search results, by default the boost value is 1. + * + * Fields should be added before any documents are added to the index, fields + * that are added after documents are added to the index will only apply to new + * documents added to the index. + * + * @param {String} fieldName The name of the field within the document that + * should be indexed + * @param {Number} boost An optional boost that can be applied to terms in this + * field. + * @returns {lunr.Index} + * @memberOf Index + */ +lunr.Index.prototype.field = function (fieldName, opts) { +  var opts = opts || {}, +      field = { name: fieldName, boost: opts.boost || 1 } + +  this._fields.push(field) +  return this +} + +/** + * Sets the property used to uniquely identify documents added to the index, + * by default this property is 'id'. + * + * This should only be changed before adding documents to the index, changing + * the ref property without resetting the index can lead to unexpected results. + * + * @param {String} refName The property to use to uniquely identify the + * documents in the index. + * @returns {lunr.Index} + * @memberOf Index + */ +lunr.Index.prototype.ref = function (refName) { +  this._ref = refName +  return this +} + +/** + * Add a document to the index. + * + * This is the way new documents enter the index, this function will run the + * fields from the document through the index's pipeline and then add it to + * the index, it will then show up in search results. + * + * @param {Object} doc The document to add to the index. + * @memberOf Index + */ +lunr.Index.prototype.add = function (doc) { +  var docTokens = {}, +      allDocumentTokens = new lunr.SortedSet, +      docRef = doc[this._ref] + +  this._fields.forEach(function (field) { +    var fieldTokens = this.pipeline.run(lunr.tokenizer(doc[field.name])) + +    docTokens[field.name] = fieldTokens +    lunr.SortedSet.prototype.add.apply(allDocumentTokens, fieldTokens) +  }, this) + +  this.documentStore.set(docRef, allDocumentTokens) +  lunr.SortedSet.prototype.add.apply(this.corpusTokens, allDocumentTokens.toArray()) + +  for (var i = 0; i < allDocumentTokens.length; i++) { +    var token = allDocumentTokens.elements[i] +    var tf = this._fields.reduce(function (memo, field) { +      var tokenCount = docTokens[field.name].filter(function (t) { return t === token }).length, +          fieldLength = docTokens[field.name].length + +      return memo + (tokenCount / fieldLength * field.boost) +    }, 0) + +    this.tokenStore.add(token, { ref: docRef, tf: tf }) +  }; +} + +/** + * Removes a document from the index. + * + * To make sure documents no longer show up in search results they can be + * removed from the index using this method. + * + * The document passed only needs to have the same ref property value as the + * document that was added to the index, they could be completely different + * objects. + * + * @param {Object} doc The document to remove from the index. + * @memberOf Index + */ +lunr.Index.prototype.remove = function (doc) { +  var docRef = doc[this._ref] + +  if (!this.documentStore.has(docRef)) return + +  var docTokens = this.documentStore.get(docRef) + +  this.documentStore.remove(docRef) + +  docTokens.forEach(function (token) { +    this.tokenStore.remove(token, docRef) +  }, this) +} + +/** + * Updates a document in the index. + * + * When a document contained within the index gets updated, fields changed, + * added or removed, to make sure it correctly matched against search queries, + * it should be updated in the index. + * + * This method is just a wrapper around `remove` and `add` + * + * @param {Object} doc The document to update in the index. + * @see Index.prototype.remove + * @see Index.prototype.add + * @memberOf Index + */ +lunr.Index.prototype.update = function (doc) { +  this.remove(doc) +  this.add(doc) +} + +/** + * Calculates the inverse document frequency for a token within the index. + * + * @param {String} token The token to calculate the idf of. + * @see Index.prototype.idf + * @private + * @memberOf Index + */ +lunr.Index.prototype.idf = function (term) { +  var documentFrequency = Object.keys(this.tokenStore.get(term)).length + +  if (documentFrequency === 0) { +    return 1 +  } else { +    return 1 + Math.log(this.tokenStore.length / documentFrequency) +  } +} + +/** + * Searches the index using the passed query. + * + * Queries should be a string, multiple words are allowed and will lead to an + * AND based query, e.g. `idx.search('foo bar')` will run a search for + * documents containing both 'foo' and 'bar'. + * + * All query tokens are passed through the same pipeline that document tokens + * are passed through, so any language processing involved will be run on every + * query term. + * + * Each query term is expanded, so that the term 'he' might be expanded to + * 'hello' and 'help' if those terms were already included in the index. + * + * Matching documents are returned as an array of objects, each object contains + * the matching document ref, as set for this index, and the similarity score + * for this document against the query. + * + * @param {String} query The query to search the index with. + * @returns {Object} + * @see Index.prototype.idf + * @see Index.prototype.documentVector + * @memberOf Index + */ +lunr.Index.prototype.search = function (query) { +  var queryTokens = this.pipeline.run(lunr.tokenizer(query)), +      queryArr = new Array (this.corpusTokens.length), +      documentSets = [], +      fieldBoosts = this._fields.reduce(function (memo, f) { return memo + f.boost }, 0) + +  var hasSomeToken = queryTokens.some(function (token) { +    return this.tokenStore.has(token) +  }, this) + +  if (!hasSomeToken) return [] + +  queryTokens +    .forEach(function (token, i, tokens) { +      var tf = 1 / tokens.length * this._fields.length * fieldBoosts, +          self = this + +      var set = this.tokenStore.expand(token).reduce(function (memo, key) { +        var pos = self.corpusTokens.indexOf(key), +            idf = self.idf(key), +            exactMatchBoost = (key === token ? 10 : 1), +            set = new lunr.SortedSet + +        // calculate the query tf-idf score for this token +        // applying an exactMatchBoost to ensure these rank +        // higher than expanded terms +        if (pos > -1) queryArr[pos] = tf * idf * exactMatchBoost + +        // add all the documents that have this key into a set +        Object.keys(self.tokenStore.get(key)).forEach(function (ref) { set.add(ref) }) + +        return memo.union(set) +      }, new lunr.SortedSet) + +      documentSets.push(set) +    }, this) + +  var documentSet = documentSets.reduce(function (memo, set) { +    return memo.intersect(set) +  }) + +  var queryVector = new lunr.Vector (queryArr) + +  return documentSet +    .map(function (ref) { +      return { ref: ref, score: queryVector.similarity(this.documentVector(ref)) } +    }, this) +    .sort(function (a, b) { +      return b.score - a.score +    }) +} + +/** + * Generates a vector containing all the tokens in the document matching the + * passed documentRef. + * + * The vector contains the tf-idf score for each token contained in the + * document with the passed documentRef.  The vector will contain an element + * for every token in the indexes corpus, if the document does not contain that + * token the element will be 0. + * + * @param {Object} documentRef The ref to find the document with. + * @returns {lunr.Vector} + * @private + * @memberOf Index + */ +lunr.Index.prototype.documentVector = function (documentRef) { +  var documentTokens = this.documentStore.get(documentRef), +      documentTokensLength = documentTokens.length, +      documentArr = new Array (this.corpusTokens.length) + +  for (var i = 0; i < documentTokensLength; i++) { +    var token = documentTokens.elements[i], +        tf = this.tokenStore.get(token)[documentRef].tf, +        idf = this.idf(token) + +    documentArr[this.corpusTokens.indexOf(token)] = tf * idf +  }; + +  return new lunr.Vector (documentArr) +} + +/** + * Returns a representation of the index ready for serialisation. + * + * @returns {Object} + * @memberOf Index + */ +lunr.Index.prototype.toJSON = function () { +  return { +    version: lunr.version, +    fields: this._fields, +    ref: this._ref, +    documentStore: this.documentStore.toJSON(), +    tokenStore: this.tokenStore.toJSON(), +    corpusTokens: this.corpusTokens.toJSON(), +    pipeline: this.pipeline.toJSON() +  } +} +/*! + * lunr.Store + * Copyright (C) 2013 Oliver Nightingale + */ + +/** + * lunr.Store is a simple key-value store used for storing sets of tokens for + * documents stored in index. + * + * @constructor + * @module + */ +lunr.Store = function () { +  this.store = {} +  this.length = 0 +} + +/** + * Loads a previously serialised store + * + * @param {Object} serialisedData The serialised store to load. + * @returns {lunr.Store} + * @memberOf Store + */ +lunr.Store.load = function (serialisedData) { +  var store = new this + +  store.length = serialisedData.length +  store.store = Object.keys(serialisedData.store).reduce(function (memo, key) { +    memo[key] = lunr.SortedSet.load(serialisedData.store[key]) +    return memo +  }, {}) + +  return store +} + +/** + * Stores the given tokens in the store against the given id. + * + * @param {Object} id The key used to store the tokens against. + * @param {Object} tokens The tokens to store against the key. + * @memberOf Store + */ +lunr.Store.prototype.set = function (id, tokens) { +  this.store[id] = tokens +  this.length = Object.keys(this.store).length +} + +/** + * Retrieves the tokens from the store for a given key. + * + * @param {Object} id The key to lookup and retrieve from the store. + * @returns {Object} + * @memberOf Store + */ +lunr.Store.prototype.get = function (id) { +  return this.store[id] +} + +/** + * Checks whether the store contains a key. + * + * @param {Object} id The id to look up in the store. + * @returns {Boolean} + * @memberOf Store + */ +lunr.Store.prototype.has = function (id) { +  return id in this.store +} + +/** + * Removes the value for a key in the store. + * + * @param {Object} id The id to remove from the store. + * @memberOf Store + */ +lunr.Store.prototype.remove = function (id) { +  if (!this.has(id)) return + +  delete this.store[id] +  this.length-- +} + +/** + * Returns a representation of the store ready for serialisation. + * + * @returns {Object} + * @memberOf Store + */ +lunr.Store.prototype.toJSON = function () { +  return { +    store: this.store, +    length: this.length +  } +} + +/*! + * lunr.stemmer + * Copyright (C) 2013 Oliver Nightingale + * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt + */ + +/** + * lunr.stemmer is an english language stemmer, this is a JavaScript + * implementation of the PorterStemmer taken from http://tartaurs.org/~martin + * + * @module + * @param {String} str The string to stem + * @returns {String} + * @see lunr.Pipeline + */ +lunr.stemmer = (function(){ +  var step2list = { +      "ational" : "ate", +      "tional" : "tion", +      "enci" : "ence", +      "anci" : "ance", +      "izer" : "ize", +      "bli" : "ble", +      "alli" : "al", +      "entli" : "ent", +      "eli" : "e", +      "ousli" : "ous", +      "ization" : "ize", +      "ation" : "ate", +      "ator" : "ate", +      "alism" : "al", +      "iveness" : "ive", +      "fulness" : "ful", +      "ousness" : "ous", +      "aliti" : "al", +      "iviti" : "ive", +      "biliti" : "ble", +      "logi" : "log" +    }, + +    step3list = { +      "icate" : "ic", +      "ative" : "", +      "alize" : "al", +      "iciti" : "ic", +      "ical" : "ic", +      "ful" : "", +      "ness" : "" +    }, + +    c = "[^aeiou]",          // consonant +    v = "[aeiouy]",          // vowel +    C = c + "[^aeiouy]*",    // consonant sequence +    V = v + "[aeiou]*",      // vowel sequence + +    mgr0 = "^(" + C + ")?" + V + C,               // [C]VC... is m>0 +    meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$",  // [C]VC[V] is m=1 +    mgr1 = "^(" + C + ")?" + V + C + V + C,       // [C]VCVC... is m>1 +    s_v = "^(" + C + ")?" + v;                   // vowel in stem + +  return function (w) { +    var   stem, +      suffix, +      firstch, +      re, +      re2, +      re3, +      re4; + +    if (w.length < 3) { return w; } + +    firstch = w.substr(0,1); +    if (firstch == "y") { +      w = firstch.toUpperCase() + w.substr(1); +    } + +    // Step 1a +    re = /^(.+?)(ss|i)es$/; +    re2 = /^(.+?)([^s])s$/; + +    if (re.test(w)) { w = w.replace(re,"$1$2"); } +    else if (re2.test(w)) { w = w.replace(re2,"$1$2"); } + +    // Step 1b +    re = /^(.+?)eed$/; +    re2 = /^(.+?)(ed|ing)$/; +    if (re.test(w)) { +      var fp = re.exec(w); +      re = new RegExp(mgr0); +      if (re.test(fp[1])) { +        re = /.$/; +        w = w.replace(re,""); +      } +    } else if (re2.test(w)) { +      var fp = re2.exec(w); +      stem = fp[1]; +      re2 = new RegExp(s_v); +      if (re2.test(stem)) { +        w = stem; +        re2 = /(at|bl|iz)$/; +        re3 = new RegExp("([^aeiouylsz])\\1$"); +        re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); +        if (re2.test(w)) {  w = w + "e"; } +        else if (re3.test(w)) { re = /.$/; w = w.replace(re,""); } +        else if (re4.test(w)) { w = w + "e"; } +      } +    } + +    // Step 1c +    re = /^(.+?)y$/; +    if (re.test(w)) { +      var fp = re.exec(w); +      stem = fp[1]; +      re = new RegExp(s_v); +      if (re.test(stem)) { w = stem + "i"; } +    } + +    // Step 2 +    re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; +    if (re.test(w)) { +      var fp = re.exec(w); +      stem = fp[1]; +      suffix = fp[2]; +      re = new RegExp(mgr0); +      if (re.test(stem)) { +        w = stem + step2list[suffix]; +      } +    } + +    // Step 3 +    re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; +    if (re.test(w)) { +      var fp = re.exec(w); +      stem = fp[1]; +      suffix = fp[2]; +      re = new RegExp(mgr0); +      if (re.test(stem)) { +        w = stem + step3list[suffix]; +      } +    } + +    // Step 4 +    re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; +    re2 = /^(.+?)(s|t)(ion)$/; +    if (re.test(w)) { +      var fp = re.exec(w); +      stem = fp[1]; +      re = new RegExp(mgr1); +      if (re.test(stem)) { +        w = stem; +      } +    } else if (re2.test(w)) { +      var fp = re2.exec(w); +      stem = fp[1] + fp[2]; +      re2 = new RegExp(mgr1); +      if (re2.test(stem)) { +        w = stem; +      } +    } + +    // Step 5 +    re = /^(.+?)e$/; +    if (re.test(w)) { +      var fp = re.exec(w); +      stem = fp[1]; +      re = new RegExp(mgr1); +      re2 = new RegExp(meq1); +      re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); +      if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) { +        w = stem; +      } +    } + +    re = /ll$/; +    re2 = new RegExp(mgr1); +    if (re.test(w) && re2.test(w)) { +      re = /.$/; +      w = w.replace(re,""); +    } + +    // and turn initial Y back to y + +    if (firstch == "y") { +      w = firstch.toLowerCase() + w.substr(1); +    } + +    return w; +  } +})(); + +lunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer') +/*! + * lunr.stopWordFilter + * Copyright (C) 2013 Oliver Nightingale + */ + +/** + * lunr.stopWordFilter is an English language stop word list filter, any words + * contained in the list will not be passed through the filter. + * + * This is intended to be used in the Pipeline. If the token does not pass the + * filter then undefined will be returned. + * + * @module + * @param {String} token The token to pass through the filter + * @returns {String} + * @see lunr.Pipeline + */ +lunr.stopWordFilter = function (token) { +  if (lunr.stopWordFilter.stopWords.indexOf(token) === -1) return token +} + +lunr.stopWordFilter.stopWords = new lunr.SortedSet +lunr.stopWordFilter.stopWords.length = 119 +lunr.stopWordFilter.stopWords.elements = [ +  "a", +  "able", +  "about", +  "across", +  "after", +  "all", +  "almost", +  "also", +  "am", +  "among", +  "an", +  "and", +  "any", +  "are", +  "as", +  "at", +  "be", +  "because", +  "been", +  "but", +  "by", +  "can", +  "cannot", +  "could", +  "dear", +  "did", +  "do", +  "does", +  "either", +  "else", +  "ever", +  "every", +  "for", +  "from", +  "get", +  "got", +  "had", +  "has", +  "have", +  "he", +  "her", +  "hers", +  "him", +  "his", +  "how", +  "however", +  "i", +  "if", +  "in", +  "into", +  "is", +  "it", +  "its", +  "just", +  "least", +  "let", +  "like", +  "likely", +  "may", +  "me", +  "might", +  "most", +  "must", +  "my", +  "neither", +  "no", +  "nor", +  "not", +  "of", +  "off", +  "often", +  "on", +  "only", +  "or", +  "other", +  "our", +  "own", +  "rather", +  "said", +  "say", +  "says", +  "she", +  "should", +  "since", +  "so", +  "some", +  "than", +  "that", +  "the", +  "their", +  "them", +  "then", +  "there", +  "these", +  "they", +  "this", +  "tis", +  "to", +  "too", +  "twas", +  "us", +  "wants", +  "was", +  "we", +  "were", +  "what", +  "when", +  "where", +  "which", +  "while", +  "who", +  "whom", +  "why", +  "will", +  "with", +  "would", +  "yet", +  "you", +  "your" +] + +lunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter') +/*! + * lunr.stemmer + * Copyright (C) 2013 Oliver Nightingale + * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt + */ + +/** + * lunr.TokenStore is used for efficient storing and lookup of the reverse + * index of token to document ref. + * + * @constructor + */ +lunr.TokenStore = function () { +  this.root = { docs: {} } +  this.length = 0 +} + +/** + * Loads a previously serialised token store + * + * @param {Object} serialisedData The serialised token store to load. + * @returns {lunr.TokenStore} + * @memberOf TokenStore + */ +lunr.TokenStore.load = function (serialisedData) { +  var store = new this + +  store.root = serialisedData.root +  store.length = serialisedData.length + +  return store +} + +/** + * Adds a new token doc pair to the store. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to store the doc under + * @param {Object} doc The doc to store against the token + * @param {Object} root An optional node at which to start looking for the + * correct place to enter the doc, by default the root of this lunr.TokenStore + * is used. + * @memberOf TokenStore + */ +lunr.TokenStore.prototype.add = function (token, doc, root) { +  var root = root || this.root, +      key = token[0], +      rest = token.slice(1) + +  if (!(key in root)) root[key] = {docs: {}} + +  if (rest.length === 0) { +    root[key].docs[doc.ref] = doc +    this.length += 1 +    return +  } else { +    return this.add(rest, doc, root[key]) +  } +} + +/** + * Checks whether this key is contained within this lunr.TokenStore. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to check for + * @param {Object} root An optional node at which to start + * @memberOf TokenStore + */ +lunr.TokenStore.prototype.has = function (token, root) { +  var root = root || this.root, +      key = token[0], +      rest = token.slice(1) + +  if (!(key in root)) return false + +  if (rest.length === 0) { +    return true +  } else { +    return this.has(rest, root[key]) +  } +} + +/** + * Retrieve a node from the token store for a given token. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to get the node for. + * @param {Object} root An optional node at which to start. + * @returns {Object} + * @see TokenStore.prototype.get + * @memberOf TokenStore + */ +lunr.TokenStore.prototype.getNode = function (token, root) { +  var root = root || this.root, +      key = token[0], +      rest = token.slice(1) + +  if (!(key in root)) return {} + +  if (rest.length === 0) { +    return root[key] +  } else { +    return this.getNode(rest, root[key]) +  } +} + +/** + * Retrieve the documents for a node for the given token. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to get the documents for. + * @param {Object} root An optional node at which to start. + * @returns {Object} + * @memberOf TokenStore + */ +lunr.TokenStore.prototype.get = function (token, root) { +  return this.getNode(token, root).docs || {} +} + +/** + * Remove the document identified by ref from the token in the store. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to get the documents for. + * @param {String} ref The ref of the document to remove from this token. + * @param {Object} root An optional node at which to start. + * @returns {Object} + * @memberOf TokenStore + */ +lunr.TokenStore.prototype.remove = function (token, ref, root) { +  var root = root || this.root, +      key = token[0], +      rest = token.slice(1) + +  if (!(key in root)) return + +  if (rest.length === 0) { +    delete root[key].docs[ref] +  } else { +    return this.remove(rest, ref, root[key]) +  } +} + +/** + * Find all the possible suffixes of the passed token using tokens + * currently in the store. + * + * @param {String} token The token to expand. + * @returns {Array} + * @memberOf TokenStore + */ +lunr.TokenStore.prototype.expand = function (token, memo) { +  var root = this.getNode(token), +      docs = root.docs || {}, +      memo = memo || [] + +  if (Object.keys(docs).length) memo.push(token) + +  Object.keys(root) +    .forEach(function (key) { +      if (key === 'docs') return + +      memo.concat(this.expand(token + key, memo)) +    }, this) + +  return memo +} + +/** + * Returns a representation of the token store ready for serialisation. + * + * @returns {Object} + * @memberOf TokenStore + */ +lunr.TokenStore.prototype.toJSON = function () { +  return { +    root: this.root, +    length: this.length +  } +} + | 
