",
253 | options: {
254 | disabled: false,
255 |
256 | // callbacks
257 | create: null
258 | },
259 | _createWidget: function( options, element ) {
260 | element = $( element || this.defaultElement || this )[ 0 ];
261 | this.element = $( element );
262 | this.uuid = widget_uuid++;
263 | this.eventNamespace = "." + this.widgetName + this.uuid;
264 |
265 | this.bindings = $();
266 | this.hoverable = $();
267 | this.focusable = $();
268 |
269 | if ( element !== this ) {
270 | $.data( element, this.widgetFullName, this );
271 | this._on( true, this.element, {
272 | remove: function( event ) {
273 | if ( event.target === element ) {
274 | this.destroy();
275 | }
276 | }
277 | });
278 | this.document = $( element.style ?
279 | // element within the document
280 | element.ownerDocument :
281 | // element is window or document
282 | element.document || element );
283 | this.window = $( this.document[0].defaultView || this.document[0].parentWindow );
284 | }
285 |
286 | this.options = $.widget.extend( {},
287 | this.options,
288 | this._getCreateOptions(),
289 | options );
290 |
291 | this._create();
292 | this._trigger( "create", null, this._getCreateEventData() );
293 | this._init();
294 | },
295 | _getCreateOptions: $.noop,
296 | _getCreateEventData: $.noop,
297 | _create: $.noop,
298 | _init: $.noop,
299 |
300 | destroy: function() {
301 | this._destroy();
302 | // we can probably remove the unbind calls in 2.0
303 | // all event bindings should go through this._on()
304 | this.element
305 | .unbind( this.eventNamespace )
306 | .removeData( this.widgetFullName )
307 | // support: jquery <1.6.3
308 | // http://bugs.jquery.com/ticket/9413
309 | .removeData( $.camelCase( this.widgetFullName ) );
310 | this.widget()
311 | .unbind( this.eventNamespace )
312 | .removeAttr( "aria-disabled" )
313 | .removeClass(
314 | this.widgetFullName + "-disabled " +
315 | "ui-state-disabled" );
316 |
317 | // clean up events and states
318 | this.bindings.unbind( this.eventNamespace );
319 | this.hoverable.removeClass( "ui-state-hover" );
320 | this.focusable.removeClass( "ui-state-focus" );
321 | },
322 | _destroy: $.noop,
323 |
324 | widget: function() {
325 | return this.element;
326 | },
327 |
328 | option: function( key, value ) {
329 | var options = key,
330 | parts,
331 | curOption,
332 | i;
333 |
334 | if ( arguments.length === 0 ) {
335 | // don't return a reference to the internal hash
336 | return $.widget.extend( {}, this.options );
337 | }
338 |
339 | if ( typeof key === "string" ) {
340 | // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } }
341 | options = {};
342 | parts = key.split( "." );
343 | key = parts.shift();
344 | if ( parts.length ) {
345 | curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] );
346 | for ( i = 0; i < parts.length - 1; i++ ) {
347 | curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {};
348 | curOption = curOption[ parts[ i ] ];
349 | }
350 | key = parts.pop();
351 | if ( arguments.length === 1 ) {
352 | return curOption[ key ] === undefined ? null : curOption[ key ];
353 | }
354 | curOption[ key ] = value;
355 | } else {
356 | if ( arguments.length === 1 ) {
357 | return this.options[ key ] === undefined ? null : this.options[ key ];
358 | }
359 | options[ key ] = value;
360 | }
361 | }
362 |
363 | this._setOptions( options );
364 |
365 | return this;
366 | },
367 | _setOptions: function( options ) {
368 | var key;
369 |
370 | for ( key in options ) {
371 | this._setOption( key, options[ key ] );
372 | }
373 |
374 | return this;
375 | },
376 | _setOption: function( key, value ) {
377 | this.options[ key ] = value;
378 |
379 | if ( key === "disabled" ) {
380 | this.widget()
381 | .toggleClass( this.widgetFullName + "-disabled", !!value );
382 |
383 | // If the widget is becoming disabled, then nothing is interactive
384 | if ( value ) {
385 | this.hoverable.removeClass( "ui-state-hover" );
386 | this.focusable.removeClass( "ui-state-focus" );
387 | }
388 | }
389 |
390 | return this;
391 | },
392 |
393 | enable: function() {
394 | return this._setOptions({ disabled: false });
395 | },
396 | disable: function() {
397 | return this._setOptions({ disabled: true });
398 | },
399 |
400 | _on: function( suppressDisabledCheck, element, handlers ) {
401 | var delegateElement,
402 | instance = this;
403 |
404 | // no suppressDisabledCheck flag, shuffle arguments
405 | if ( typeof suppressDisabledCheck !== "boolean" ) {
406 | handlers = element;
407 | element = suppressDisabledCheck;
408 | suppressDisabledCheck = false;
409 | }
410 |
411 | // no element argument, shuffle and use this.element
412 | if ( !handlers ) {
413 | handlers = element;
414 | element = this.element;
415 | delegateElement = this.widget();
416 | } else {
417 | element = delegateElement = $( element );
418 | this.bindings = this.bindings.add( element );
419 | }
420 |
421 | $.each( handlers, function( event, handler ) {
422 | function handlerProxy() {
423 | // allow widgets to customize the disabled handling
424 | // - disabled as an array instead of boolean
425 | // - disabled class as method for disabling individual parts
426 | if ( !suppressDisabledCheck &&
427 | ( instance.options.disabled === true ||
428 | $( this ).hasClass( "ui-state-disabled" ) ) ) {
429 | return;
430 | }
431 | return ( typeof handler === "string" ? instance[ handler ] : handler )
432 | .apply( instance, arguments );
433 | }
434 |
435 | // copy the guid so direct unbinding works
436 | if ( typeof handler !== "string" ) {
437 | handlerProxy.guid = handler.guid =
438 | handler.guid || handlerProxy.guid || $.guid++;
439 | }
440 |
441 | var match = event.match( /^([\w:-]*)\s*(.*)$/ ),
442 | eventName = match[1] + instance.eventNamespace,
443 | selector = match[2];
444 | if ( selector ) {
445 | delegateElement.delegate( selector, eventName, handlerProxy );
446 | } else {
447 | element.bind( eventName, handlerProxy );
448 | }
449 | });
450 | },
451 |
452 | _off: function( element, eventName ) {
453 | eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) +
454 | this.eventNamespace;
455 | element.unbind( eventName ).undelegate( eventName );
456 |
457 | // Clear the stack to avoid memory leaks (#10056)
458 | this.bindings = $( this.bindings.not( element ).get() );
459 | this.focusable = $( this.focusable.not( element ).get() );
460 | this.hoverable = $( this.hoverable.not( element ).get() );
461 | },
462 |
463 | _delay: function( handler, delay ) {
464 | function handlerProxy() {
465 | return ( typeof handler === "string" ? instance[ handler ] : handler )
466 | .apply( instance, arguments );
467 | }
468 | var instance = this;
469 | return setTimeout( handlerProxy, delay || 0 );
470 | },
471 |
472 | _hoverable: function( element ) {
473 | this.hoverable = this.hoverable.add( element );
474 | this._on( element, {
475 | mouseenter: function( event ) {
476 | $( event.currentTarget ).addClass( "ui-state-hover" );
477 | },
478 | mouseleave: function( event ) {
479 | $( event.currentTarget ).removeClass( "ui-state-hover" );
480 | }
481 | });
482 | },
483 |
484 | _focusable: function( element ) {
485 | this.focusable = this.focusable.add( element );
486 | this._on( element, {
487 | focusin: function( event ) {
488 | $( event.currentTarget ).addClass( "ui-state-focus" );
489 | },
490 | focusout: function( event ) {
491 | $( event.currentTarget ).removeClass( "ui-state-focus" );
492 | }
493 | });
494 | },
495 |
496 | _trigger: function( type, event, data ) {
497 | var prop, orig,
498 | callback = this.options[ type ];
499 |
500 | data = data || {};
501 | event = $.Event( event );
502 | event.type = ( type === this.widgetEventPrefix ?
503 | type :
504 | this.widgetEventPrefix + type ).toLowerCase();
505 | // the original event may come from any element
506 | // so we need to reset the target on the new event
507 | event.target = this.element[ 0 ];
508 |
509 | // copy original event properties over to the new event
510 | orig = event.originalEvent;
511 | if ( orig ) {
512 | for ( prop in orig ) {
513 | if ( !( prop in event ) ) {
514 | event[ prop ] = orig[ prop ];
515 | }
516 | }
517 | }
518 |
519 | this.element.trigger( event, data );
520 | return !( $.isFunction( callback ) &&
521 | callback.apply( this.element[0], [ event ].concat( data ) ) === false ||
522 | event.isDefaultPrevented() );
523 | }
524 | };
525 |
526 | $.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) {
527 | $.Widget.prototype[ "_" + method ] = function( element, options, callback ) {
528 | if ( typeof options === "string" ) {
529 | options = { effect: options };
530 | }
531 | var hasOptions,
532 | effectName = !options ?
533 | method :
534 | options === true || typeof options === "number" ?
535 | defaultEffect :
536 | options.effect || defaultEffect;
537 | options = options || {};
538 | if ( typeof options === "number" ) {
539 | options = { duration: options };
540 | }
541 | hasOptions = !$.isEmptyObject( options );
542 | options.complete = callback;
543 | if ( options.delay ) {
544 | element.delay( options.delay );
545 | }
546 | if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) {
547 | element[ method ]( options );
548 | } else if ( effectName !== method && element[ effectName ] ) {
549 | element[ effectName ]( options.duration, options.easing, callback );
550 | } else {
551 | element.queue(function( next ) {
552 | $( this )[ method ]();
553 | if ( callback ) {
554 | callback.call( element[ 0 ] );
555 | }
556 | next();
557 | });
558 | }
559 | };
560 | });
561 |
562 | var widget = $.widget;
563 |
564 |
565 |
566 | }));
567 |
--------------------------------------------------------------------------------
/source/javascripts/lib/jquery.tocify.js:
--------------------------------------------------------------------------------
1 | //= require ./jquery_ui
2 | /* jquery Tocify - v1.8.0 - 2013-09-16
3 | * http://www.gregfranko.com/jquery.tocify.js/
4 | * Copyright (c) 2013 Greg Franko; Licensed MIT
5 | * Modified lightly by Robert Lord to fix a bug I found,
6 | * and also so it adds ids to headers
7 | * also because I want height caching, since the
8 | * height lookup for h1s and h2s was causing serious
9 | * lag spikes below 30 fps */
10 |
11 | // Immediately-Invoked Function Expression (IIFE) [Ben Alman Blog Post](http://benalman.com/news/2010/11/immediately-invoked-function-expression/) that calls another IIFE that contains all of the plugin logic. I used this pattern so that anyone viewing this code would not have to scroll to the bottom of the page to view the local parameters that were passed to the main IIFE.
12 | (function(tocify) {
13 |
14 | // ECMAScript 5 Strict Mode: [John Resig Blog Post](http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/)
15 | "use strict";
16 |
17 | // Calls the second IIFE and locally passes in the global jQuery, window, and document objects
18 | tocify(window.jQuery, window, document);
19 |
20 | }
21 |
22 | // Locally passes in `jQuery`, the `window` object, the `document` object, and an `undefined` variable. The `jQuery`, `window` and `document` objects are passed in locally, to improve performance, since javascript first searches for a variable match within the local variables set before searching the global variables set. All of the global variables are also passed in locally to be minifier friendly. `undefined` can be passed in locally, because it is not a reserved word in JavaScript.
23 | (function($, window, document, undefined) {
24 |
25 | // ECMAScript 5 Strict Mode: [John Resig Blog Post](http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/)
26 | "use strict";
27 |
28 | var tocClassName = "tocify",
29 | tocClass = "." + tocClassName,
30 | tocFocusClassName = "tocify-focus",
31 | tocHoverClassName = "tocify-hover",
32 | hideTocClassName = "tocify-hide",
33 | hideTocClass = "." + hideTocClassName,
34 | headerClassName = "tocify-header",
35 | headerClass = "." + headerClassName,
36 | subheaderClassName = "tocify-subheader",
37 | subheaderClass = "." + subheaderClassName,
38 | itemClassName = "tocify-item",
39 | itemClass = "." + itemClassName,
40 | extendPageClassName = "tocify-extend-page",
41 | extendPageClass = "." + extendPageClassName;
42 |
43 | // Calling the jQueryUI Widget Factory Method
44 | $.widget("toc.tocify", {
45 |
46 | //Plugin version
47 | version: "1.8.0",
48 |
49 | // These options will be used as defaults
50 | options: {
51 |
52 | // **context**: Accepts String: Any jQuery selector
53 | // The container element that holds all of the elements used to generate the table of contents
54 | context: "body",
55 |
56 | // **ignoreSelector**: Accepts String: Any jQuery selector
57 | // A selector to any element that would be matched by selectors that you wish to be ignored
58 | ignoreSelector: null,
59 |
60 | // **selectors**: Accepts an Array of Strings: Any jQuery selectors
61 | // The element's used to generate the table of contents. The order is very important since it will determine the table of content's nesting structure
62 | selectors: "h1, h2, h3",
63 |
64 | // **showAndHide**: Accepts a boolean: true or false
65 | // Used to determine if elements should be shown and hidden
66 | showAndHide: true,
67 |
68 | // **showEffect**: Accepts String: "none", "fadeIn", "show", or "slideDown"
69 | // Used to display any of the table of contents nested items
70 | showEffect: "slideDown",
71 |
72 | // **showEffectSpeed**: Accepts Number (milliseconds) or String: "slow", "medium", or "fast"
73 | // The time duration of the show animation
74 | showEffectSpeed: "medium",
75 |
76 | // **hideEffect**: Accepts String: "none", "fadeOut", "hide", or "slideUp"
77 | // Used to hide any of the table of contents nested items
78 | hideEffect: "slideUp",
79 |
80 | // **hideEffectSpeed**: Accepts Number (milliseconds) or String: "slow", "medium", or "fast"
81 | // The time duration of the hide animation
82 | hideEffectSpeed: "medium",
83 |
84 | // **smoothScroll**: Accepts a boolean: true or false
85 | // Determines if a jQuery animation should be used to scroll to specific table of contents items on the page
86 | smoothScroll: true,
87 |
88 | // **smoothScrollSpeed**: Accepts Number (milliseconds) or String: "slow", "medium", or "fast"
89 | // The time duration of the smoothScroll animation
90 | smoothScrollSpeed: "medium",
91 |
92 | // **scrollTo**: Accepts Number (pixels)
93 | // The amount of space between the top of page and the selected table of contents item after the page has been scrolled
94 | scrollTo: 0,
95 |
96 | // **showAndHideOnScroll**: Accepts a boolean: true or false
97 | // Determines if table of contents nested items should be shown and hidden while scrolling
98 | showAndHideOnScroll: true,
99 |
100 | // **highlightOnScroll**: Accepts a boolean: true or false
101 | // Determines if table of contents nested items should be highlighted (set to a different color) while scrolling
102 | highlightOnScroll: true,
103 |
104 | // **highlightOffset**: Accepts a number
105 | // The offset distance in pixels to trigger the next active table of contents item
106 | highlightOffset: 40,
107 |
108 | // **theme**: Accepts a string: "bootstrap", "jqueryui", or "none"
109 | // Determines if Twitter Bootstrap, jQueryUI, or Tocify classes should be added to the table of contents
110 | theme: "bootstrap",
111 |
112 | // **extendPage**: Accepts a boolean: true or false
113 | // If a user scrolls to the bottom of the page and the page is not tall enough to scroll to the last table of contents item, then the page height is increased
114 | extendPage: true,
115 |
116 | // **extendPageOffset**: Accepts a number: pixels
117 | // How close to the bottom of the page a user must scroll before the page is extended
118 | extendPageOffset: 100,
119 |
120 | // **history**: Accepts a boolean: true or false
121 | // Adds a hash to the page url to maintain history
122 | history: true,
123 |
124 | // **scrollHistory**: Accepts a boolean: true or false
125 | // Adds a hash to the page url, to maintain history, when scrolling to a TOC item
126 | scrollHistory: false,
127 |
128 | // **hashGenerator**: How the hash value (the anchor segment of the URL, following the
129 | // # character) will be generated.
130 | //
131 | // "compact" (default) - #CompressesEverythingTogether
132 | // "pretty" - #looks-like-a-nice-url-and-is-easily-readable
133 | // function(text, element){} - Your own hash generation function that accepts the text as an
134 | // argument, and returns the hash value.
135 | hashGenerator: "compact",
136 |
137 | // **highlightDefault**: Accepts a boolean: true or false
138 | // Set's the first TOC item as active if no other TOC item is active.
139 | highlightDefault: true
140 |
141 | },
142 |
143 | // _Create
144 | // -------
145 | // Constructs the plugin. Only called once.
146 | _create: function() {
147 |
148 | var self = this;
149 |
150 | self.tocifyWrapper = $('.tocify-wrapper');
151 | self.extendPageScroll = true;
152 |
153 | // Internal array that keeps track of all TOC items (Helps to recognize if there are duplicate TOC item strings)
154 | self.items = [];
155 |
156 | // Generates the HTML for the dynamic table of contents
157 | self._generateToc();
158 |
159 | // Caches heights and anchors
160 | self.cachedHeights = [],
161 | self.cachedAnchors = [];
162 |
163 | // Adds CSS classes to the newly generated table of contents HTML
164 | self._addCSSClasses();
165 |
166 | self.webkit = (function() {
167 |
168 | for(var prop in window) {
169 |
170 | if(prop) {
171 |
172 | if(prop.toLowerCase().indexOf("webkit") !== -1) {
173 |
174 | return true;
175 |
176 | }
177 |
178 | }
179 |
180 | }
181 |
182 | return false;
183 |
184 | }());
185 |
186 | // Adds jQuery event handlers to the newly generated table of contents
187 | self._setEventHandlers();
188 |
189 | // Binding to the Window load event to make sure the correct scrollTop is calculated
190 | $(window).load(function() {
191 |
192 | // Sets the active TOC item
193 | self._setActiveElement(true);
194 |
195 | // Once all animations on the page are complete, this callback function will be called
196 | $("html, body").promise().done(function() {
197 |
198 | setTimeout(function() {
199 |
200 | self.extendPageScroll = false;
201 |
202 | },0);
203 |
204 | });
205 |
206 | });
207 |
208 | },
209 |
210 | // _generateToc
211 | // ------------
212 | // Generates the HTML for the dynamic table of contents
213 | _generateToc: function() {
214 |
215 | // _Local variables_
216 |
217 | // Stores the plugin context in the self variable
218 | var self = this,
219 |
220 | // All of the HTML tags found within the context provided (i.e. body) that match the top level jQuery selector above
221 | firstElem,
222 |
223 | // Instantiated variable that will store the top level newly created unordered list DOM element
224 | ul,
225 | ignoreSelector = self.options.ignoreSelector;
226 |
227 | // If the selectors option has a comma within the string
228 | if(this.options.selectors.indexOf(",") !== -1) {
229 |
230 | // Grabs the first selector from the string
231 | firstElem = $(this.options.context).find(this.options.selectors.replace(/ /g,"").substr(0, this.options.selectors.indexOf(",")));
232 |
233 | }
234 |
235 | // If the selectors option does not have a comman within the string
236 | else {
237 |
238 | // Grabs the first selector from the string and makes sure there are no spaces
239 | firstElem = $(this.options.context).find(this.options.selectors.replace(/ /g,""));
240 |
241 | }
242 |
243 | if(!firstElem.length) {
244 |
245 | self.element.addClass(hideTocClassName);
246 |
247 | return;
248 |
249 | }
250 |
251 | self.element.addClass(tocClassName);
252 |
253 | // Loops through each top level selector
254 | firstElem.each(function(index) {
255 |
256 | //If the element matches the ignoreSelector then we skip it
257 | if($(this).is(ignoreSelector)) {
258 | return;
259 | }
260 |
261 | // Creates an unordered list HTML element and adds a dynamic ID and standard class name
262 | ul = $("
", {
263 | "id": headerClassName + index,
264 | "class": headerClassName
265 | }).
266 |
267 | // Appends a top level list item HTML element to the previously created HTML header
268 | append(self._nestElements($(this), index));
269 |
270 | // Add the created unordered list element to the HTML element calling the plugin
271 | self.element.append(ul);
272 |
273 | // Finds all of the HTML tags between the header and subheader elements
274 | $(this).nextUntil(this.nodeName.toLowerCase()).each(function() {
275 |
276 | // If there are no nested subheader elemements
277 | if($(this).find(self.options.selectors).length === 0) {
278 |
279 | // Loops through all of the subheader elements
280 | $(this).filter(self.options.selectors).each(function() {
281 |
282 | //If the element matches the ignoreSelector then we skip it
283 | if($(this).is(ignoreSelector)) {
284 | return;
285 | }
286 |
287 | self._appendSubheaders.call(this, self, ul);
288 |
289 | });
290 |
291 | }
292 |
293 | // If there are nested subheader elements
294 | else {
295 |
296 | // Loops through all of the subheader elements
297 | $(this).find(self.options.selectors).each(function() {
298 |
299 | //If the element matches the ignoreSelector then we skip it
300 | if($(this).is(ignoreSelector)) {
301 | return;
302 | }
303 |
304 | self._appendSubheaders.call(this, self, ul);
305 |
306 | });
307 |
308 | }
309 |
310 | });
311 |
312 | });
313 |
314 | },
315 |
316 | _setActiveElement: function(pageload) {
317 |
318 | var self = this,
319 |
320 | hash = window.location.hash.substring(1),
321 |
322 | elem = self.element.find("li[data-unique='" + hash + "']");
323 |
324 | if(hash.length) {
325 |
326 | // Removes highlighting from all of the list item's
327 | self.element.find("." + self.focusClass).removeClass(self.focusClass);
328 |
329 | // Highlights the current list item that was clicked
330 | elem.addClass(self.focusClass);
331 |
332 | // If the showAndHide option is true
333 | if(self.options.showAndHide) {
334 |
335 | // Triggers the click event on the currently focused TOC item
336 | elem.click();
337 |
338 | }
339 |
340 | }
341 |
342 | else {
343 |
344 | // Removes highlighting from all of the list item's
345 | self.element.find("." + self.focusClass).removeClass(self.focusClass);
346 |
347 | if(!hash.length && pageload && self.options.highlightDefault) {
348 |
349 | // Highlights the first TOC item if no other items are highlighted
350 | self.element.find(itemClass).first().addClass(self.focusClass);
351 |
352 | }
353 |
354 | }
355 |
356 | return self;
357 |
358 | },
359 |
360 | // _nestElements
361 | // -------------
362 | // Helps create the table of contents list by appending nested list items
363 | _nestElements: function(self, index) {
364 |
365 | var arr, item, hashValue;
366 |
367 | arr = $.grep(this.items, function (item) {
368 |
369 | return item === self.text();
370 |
371 | });
372 |
373 | // If there is already a duplicate TOC item
374 | if(arr.length) {
375 |
376 | // Adds the current TOC item text and index (for slight randomization) to the internal array
377 | this.items.push(self.text() + index);
378 |
379 | }
380 |
381 | // If there not a duplicate TOC item
382 | else {
383 |
384 | // Adds the current TOC item text to the internal array
385 | this.items.push(self.text());
386 |
387 | }
388 |
389 | hashValue = this._generateHashValue(arr, self, index);
390 |
391 | // ADDED BY ROBERT
392 | // actually add the hash value to the element's id
393 | // self.attr("id", "link-" + hashValue);
394 |
395 | // Appends a list item HTML element to the last unordered list HTML element found within the HTML element calling the plugin
396 | item = $("
", {
397 |
398 | // Sets a common class name to the list item
399 | "class": itemClassName,
400 |
401 | "data-unique": hashValue
402 |
403 | }).append($("
", {
404 |
405 | "text": self.text()
406 |
407 | }));
408 |
409 | // Adds an HTML anchor tag before the currently traversed HTML element
410 | self.before($("
", {
411 |
412 | // Sets a name attribute on the anchor tag to the text of the currently traversed HTML element (also making sure that all whitespace is replaced with an underscore)
413 | "name": hashValue,
414 |
415 | "data-unique": hashValue
416 |
417 | }));
418 |
419 | return item;
420 |
421 | },
422 |
423 | // _generateHashValue
424 | // ------------------
425 | // Generates the hash value that will be used to refer to each item.
426 | _generateHashValue: function(arr, self, index) {
427 |
428 | var hashValue = "",
429 | hashGeneratorOption = this.options.hashGenerator;
430 |
431 | if (hashGeneratorOption === "pretty") {
432 | // remove weird characters
433 |
434 |
435 | // prettify the text
436 | hashValue = self.text().toLowerCase().replace(/\s/g, "-");
437 |
438 | // ADDED BY ROBERT
439 | // remove weird characters
440 | hashValue = hashValue.replace(/[^\x00-\x7F]/g, "");
441 |
442 | // fix double hyphens
443 | while (hashValue.indexOf("--") > -1) {
444 | hashValue = hashValue.replace(/--/g, "-");
445 | }
446 |
447 | // fix colon-space instances
448 | while (hashValue.indexOf(":-") > -1) {
449 | hashValue = hashValue.replace(/:-/g, "-");
450 | }
451 |
452 | } else if (typeof hashGeneratorOption === "function") {
453 |
454 | // call the function
455 | hashValue = hashGeneratorOption(self.text(), self);
456 |
457 | } else {
458 |
459 | // compact - the default
460 | hashValue = self.text().replace(/\s/g, "");
461 |
462 | }
463 |
464 | // add the index if we need to
465 | if (arr.length) { hashValue += ""+index; }
466 |
467 | // return the value
468 | return hashValue;
469 |
470 | },
471 |
472 | // _appendElements
473 | // ---------------
474 | // Helps create the table of contents list by appending subheader elements
475 |
476 | _appendSubheaders: function(self, ul) {
477 |
478 | // The current element index
479 | var index = $(this).index(self.options.selectors),
480 |
481 | // Finds the previous header DOM element
482 | previousHeader = $(self.options.selectors).eq(index - 1),
483 |
484 | currentTagName = +$(this).prop("tagName").charAt(1),
485 |
486 | previousTagName = +previousHeader.prop("tagName").charAt(1),
487 |
488 | lastSubheader;
489 |
490 | // If the current header DOM element is smaller than the previous header DOM element or the first subheader
491 | if(currentTagName < previousTagName) {
492 |
493 | // Selects the last unordered list HTML found within the HTML element calling the plugin
494 | self.element.find(subheaderClass + "[data-tag=" + currentTagName + "]").last().append(self._nestElements($(this), index));
495 |
496 | }
497 |
498 | // If the current header DOM element is the same type of header(eg. h4) as the previous header DOM element
499 | else if(currentTagName === previousTagName) {
500 |
501 | ul.find(itemClass).last().after(self._nestElements($(this), index));
502 |
503 | }
504 |
505 | else {
506 |
507 | // Selects the last unordered list HTML found within the HTML element calling the plugin
508 | ul.find(itemClass).last().
509 |
510 | // Appends an unorderedList HTML element to the dynamic `unorderedList` variable and sets a common class name
511 | after($("
", {
512 |
513 | "class": subheaderClassName,
514 |
515 | "data-tag": currentTagName
516 |
517 | })).next(subheaderClass).
518 |
519 | // Appends a list item HTML element to the last unordered list HTML element found within the HTML element calling the plugin
520 | append(self._nestElements($(this), index));
521 | }
522 |
523 | },
524 |
525 | // _setEventHandlers
526 | // ----------------
527 | // Adds jQuery event handlers to the newly generated table of contents
528 | _setEventHandlers: function() {
529 |
530 | // _Local variables_
531 |
532 | // Stores the plugin context in the self variable
533 | var self = this,
534 |
535 | // Instantiates a new variable that will be used to hold a specific element's context
536 | $self,
537 |
538 | // Instantiates a new variable that will be used to determine the smoothScroll animation time duration
539 | duration;
540 |
541 | // Event delegation that looks for any clicks on list item elements inside of the HTML element calling the plugin
542 | this.element.on("click.tocify", "li", function(event) {
543 |
544 | if(self.options.history) {
545 |
546 | window.location.hash = $(this).attr("data-unique");
547 |
548 | }
549 |
550 | // Removes highlighting from all of the list item's
551 | self.element.find("." + self.focusClass).removeClass(self.focusClass);
552 |
553 | // Highlights the current list item that was clicked
554 | $(this).addClass(self.focusClass);
555 |
556 | // If the showAndHide option is true
557 | if(self.options.showAndHide) {
558 |
559 | var elem = $('li[data-unique="' + $(this).attr("data-unique") + '"]');
560 |
561 | self._triggerShow(elem);
562 |
563 | }
564 |
565 | self._scrollTo($(this));
566 |
567 | });
568 |
569 | // Mouseenter and Mouseleave event handlers for the list item's within the HTML element calling the plugin
570 | this.element.find("li").on({
571 |
572 | // Mouseenter event handler
573 | "mouseenter.tocify": function() {
574 |
575 | // Adds a hover CSS class to the current list item
576 | $(this).addClass(self.hoverClass);
577 |
578 | // Makes sure the cursor is set to the pointer icon
579 | $(this).css("cursor", "pointer");
580 |
581 | },
582 |
583 | // Mouseleave event handler
584 | "mouseleave.tocify": function() {
585 |
586 | if(self.options.theme !== "bootstrap") {
587 |
588 | // Removes the hover CSS class from the current list item
589 | $(this).removeClass(self.hoverClass);
590 |
591 | }
592 |
593 | }
594 | });
595 |
596 | // Reset height cache on scroll
597 |
598 | $(window).on('resize', function() {
599 | self.calculateHeights();
600 | });
601 |
602 | // Window scroll event handler
603 | $(window).on("scroll.tocify", function() {
604 |
605 | // Once all animations on the page are complete, this callback function will be called
606 | $("html, body").promise().done(function() {
607 |
608 | // Local variables
609 |
610 | // Stores how far the user has scrolled
611 | var winScrollTop = $(window).scrollTop(),
612 |
613 | // Stores the height of the window
614 | winHeight = $(window).height(),
615 |
616 | // Stores the height of the document
617 | docHeight = $(document).height(),
618 |
619 | scrollHeight = $("body")[0].scrollHeight,
620 |
621 | // Instantiates a variable that will be used to hold a selected HTML element
622 | elem,
623 |
624 | lastElem,
625 |
626 | lastElemOffset,
627 |
628 | currentElem;
629 |
630 | if(self.options.extendPage) {
631 |
632 | // If the user has scrolled to the bottom of the page and the last toc item is not focused
633 | if((self.webkit && winScrollTop >= scrollHeight - winHeight - self.options.extendPageOffset) || (!self.webkit && winHeight + winScrollTop > docHeight - self.options.extendPageOffset)) {
634 |
635 | if(!$(extendPageClass).length) {
636 |
637 | lastElem = $('div[data-unique="' + $(itemClass).last().attr("data-unique") + '"]');
638 |
639 | if(!lastElem.length) return;
640 |
641 | // Gets the top offset of the page header that is linked to the last toc item
642 | lastElemOffset = lastElem.offset().top;
643 |
644 | // Appends a div to the bottom of the page and sets the height to the difference of the window scrollTop and the last element's position top offset
645 | $(self.options.context).append($("
", {
646 |
647 | "class": extendPageClassName,
648 |
649 | "height": Math.abs(lastElemOffset - winScrollTop) + "px",
650 |
651 | "data-unique": extendPageClassName
652 |
653 | }));
654 |
655 | if(self.extendPageScroll) {
656 |
657 | currentElem = self.element.find('li.active');
658 |
659 | self._scrollTo($("div[data-unique=" + currentElem.attr("data-unique") + "]"));
660 |
661 | }
662 |
663 | }
664 |
665 | }
666 |
667 | }
668 |
669 | // The zero timeout ensures the following code is run after the scroll events
670 | setTimeout(function() {
671 |
672 | // _Local variables_
673 |
674 | // Stores the distance to the closest anchor
675 | var // Stores the index of the closest anchor
676 | closestAnchorIdx = null,
677 | anchorText;
678 |
679 | // if never calculated before, calculate and cache the heights
680 | if (self.cachedHeights.length == 0) {
681 | self.calculateHeights();
682 | }
683 |
684 | var scrollTop = $(window).scrollTop();
685 |
686 | // Determines the index of the closest anchor
687 | self.cachedAnchors.each(function(idx) {
688 | if (self.cachedHeights[idx] - scrollTop < 0) {
689 | closestAnchorIdx = idx;
690 | } else {
691 | return false;
692 | }
693 | });
694 |
695 | anchorText = $(self.cachedAnchors[closestAnchorIdx]).attr("data-unique");
696 |
697 | // Stores the list item HTML element that corresponds to the currently traversed anchor tag
698 | elem = $('li[data-unique="' + anchorText + '"]');
699 |
700 | // If the `highlightOnScroll` option is true and a next element is found
701 | if(self.options.highlightOnScroll && elem.length && !elem.hasClass(self.focusClass)) {
702 |
703 | // Removes highlighting from all of the list item's
704 | self.element.find("." + self.focusClass).removeClass(self.focusClass);
705 |
706 | // Highlights the corresponding list item
707 | elem.addClass(self.focusClass);
708 |
709 | // Scroll to highlighted element's header
710 | var tocifyWrapper = self.tocifyWrapper;
711 | var scrollToElem = $(elem).closest('.tocify-header');
712 |
713 | var elementOffset = scrollToElem.offset().top,
714 | wrapperOffset = tocifyWrapper.offset().top;
715 | var offset = elementOffset - wrapperOffset;
716 |
717 | if (offset >= $(window).height()) {
718 | var scrollPosition = offset + tocifyWrapper.scrollTop();
719 | tocifyWrapper.scrollTop(scrollPosition);
720 | } else if (offset < 0) {
721 | tocifyWrapper.scrollTop(0);
722 | }
723 | }
724 |
725 | if(self.options.scrollHistory) {
726 |
727 | // IF STATEMENT ADDED BY ROBERT
728 |
729 | if(window.location.hash !== "#" + anchorText && anchorText !== undefined) {
730 |
731 | if(history.replaceState) {
732 | history.replaceState({}, "", "#" + anchorText);
733 | // provide a fallback
734 | } else {
735 | scrollV = document.body.scrollTop;
736 | scrollH = document.body.scrollLeft;
737 | location.hash = "#" + anchorText;
738 | document.body.scrollTop = scrollV;
739 | document.body.scrollLeft = scrollH;
740 | }
741 |
742 | }
743 |
744 | }
745 |
746 | // If the `showAndHideOnScroll` option is true
747 | if(self.options.showAndHideOnScroll && self.options.showAndHide) {
748 |
749 | self._triggerShow(elem, true);
750 |
751 | }
752 |
753 | }, 0);
754 |
755 | });
756 |
757 | });
758 |
759 | },
760 |
761 | // calculateHeights
762 | // ----
763 | // ADDED BY ROBERT
764 | calculateHeights: function() {
765 | var self = this;
766 | self.cachedHeights = [];
767 | self.cachedAnchors = [];
768 | var anchors = $(self.options.context).find("div[data-unique]");
769 | anchors.each(function(idx) {
770 | var distance = (($(this).next().length ? $(this).next() : $(this)).offset().top - self.options.highlightOffset);
771 | self.cachedHeights[idx] = distance;
772 | });
773 | self.cachedAnchors = anchors;
774 | },
775 |
776 | // Show
777 | // ----
778 | // Opens the current sub-header
779 | show: function(elem, scroll) {
780 |
781 | // Stores the plugin context in the `self` variable
782 | var self = this,
783 | element = elem;
784 |
785 | // If the sub-header is not already visible
786 | if (!elem.is(":visible")) {
787 |
788 | // If the current element does not have any nested subheaders, is not a header, and its parent is not visible
789 | if(!elem.find(subheaderClass).length && !elem.parent().is(headerClass) && !elem.parent().is(":visible")) {
790 |
791 | // Sets the current element to all of the subheaders within the current header
792 | elem = elem.parents(subheaderClass).add(elem);
793 |
794 | }
795 |
796 | // If the current element does not have any nested subheaders and is not a header
797 | else if(!elem.children(subheaderClass).length && !elem.parent().is(headerClass)) {
798 |
799 | // Sets the current element to the closest subheader
800 | elem = elem.closest(subheaderClass);
801 |
802 | }
803 |
804 | //Determines what jQuery effect to use
805 | switch (self.options.showEffect) {
806 |
807 | //Uses `no effect`
808 | case "none":
809 |
810 | elem.show();
811 |
812 | break;
813 |
814 | //Uses the jQuery `show` special effect
815 | case "show":
816 |
817 | elem.show(self.options.showEffectSpeed);
818 |
819 | break;
820 |
821 | //Uses the jQuery `slideDown` special effect
822 | case "slideDown":
823 |
824 | elem.slideDown(self.options.showEffectSpeed);
825 |
826 | break;
827 |
828 | //Uses the jQuery `fadeIn` special effect
829 | case "fadeIn":
830 |
831 | elem.fadeIn(self.options.showEffectSpeed);
832 |
833 | break;
834 |
835 | //If none of the above options were passed, then a `jQueryUI show effect` is expected
836 | default:
837 |
838 | elem.show();
839 |
840 | break;
841 |
842 | }
843 |
844 | }
845 |
846 | // If the current subheader parent element is a header
847 | if(elem.parent().is(headerClass)) {
848 |
849 | // Hides all non-active sub-headers
850 | self.hide($(subheaderClass).not(elem));
851 |
852 | }
853 |
854 | // If the current subheader parent element is not a header
855 | else {
856 |
857 | // Hides all non-active sub-headers
858 | self.hide($(subheaderClass).not(elem.closest(headerClass).find(subheaderClass).not(elem.siblings())));
859 |
860 | }
861 |
862 | // Maintains chainablity
863 | return self;
864 |
865 | },
866 |
867 | // Hide
868 | // ----
869 | // Closes the current sub-header
870 | hide: function(elem) {
871 |
872 | // Stores the plugin context in the `self` variable
873 | var self = this;
874 |
875 | //Determines what jQuery effect to use
876 | switch (self.options.hideEffect) {
877 |
878 | // Uses `no effect`
879 | case "none":
880 |
881 | elem.hide();
882 |
883 | break;
884 |
885 | // Uses the jQuery `hide` special effect
886 | case "hide":
887 |
888 | elem.hide(self.options.hideEffectSpeed);
889 |
890 | break;
891 |
892 | // Uses the jQuery `slideUp` special effect
893 | case "slideUp":
894 |
895 | elem.slideUp(self.options.hideEffectSpeed);
896 |
897 | break;
898 |
899 | // Uses the jQuery `fadeOut` special effect
900 | case "fadeOut":
901 |
902 | elem.fadeOut(self.options.hideEffectSpeed);
903 |
904 | break;
905 |
906 | // If none of the above options were passed, then a `jqueryUI hide effect` is expected
907 | default:
908 |
909 | elem.hide();
910 |
911 | break;
912 |
913 | }
914 |
915 | // Maintains chainablity
916 | return self;
917 | },
918 |
919 | // _triggerShow
920 | // ------------
921 | // Determines what elements get shown on scroll and click
922 | _triggerShow: function(elem, scroll) {
923 |
924 | var self = this;
925 |
926 | // If the current element's parent is a header element or the next element is a nested subheader element
927 | if(elem.parent().is(headerClass) || elem.next().is(subheaderClass)) {
928 |
929 | // Shows the next sub-header element
930 | self.show(elem.next(subheaderClass), scroll);
931 |
932 | }
933 |
934 | // If the current element's parent is a subheader element
935 | else if(elem.parent().is(subheaderClass)) {
936 |
937 | // Shows the parent sub-header element
938 | self.show(elem.parent(), scroll);
939 |
940 | }
941 |
942 | // Maintains chainability
943 | return self;
944 |
945 | },
946 |
947 | // _addCSSClasses
948 | // --------------
949 | // Adds CSS classes to the newly generated table of contents HTML
950 | _addCSSClasses: function() {
951 |
952 | // If the user wants a jqueryUI theme
953 | if(this.options.theme === "jqueryui") {
954 |
955 | this.focusClass = "ui-state-default";
956 |
957 | this.hoverClass = "ui-state-hover";
958 |
959 | //Adds the default styling to the dropdown list
960 | this.element.addClass("ui-widget").find(".toc-title").addClass("ui-widget-header").end().find("li").addClass("ui-widget-content");
961 |
962 | }
963 |
964 | // If the user wants a twitterBootstrap theme
965 | else if(this.options.theme === "bootstrap") {
966 |
967 | this.element.find(headerClass + "," + subheaderClass).addClass("nav nav-list");
968 |
969 | this.focusClass = "active";
970 |
971 | }
972 |
973 | // If a user does not want a prebuilt theme
974 | else {
975 |
976 | // Adds more neutral classes (instead of jqueryui)
977 |
978 | this.focusClass = tocFocusClassName;
979 |
980 | this.hoverClass = tocHoverClassName;
981 |
982 | }
983 |
984 | //Maintains chainability
985 | return this;
986 |
987 | },
988 |
989 | // setOption
990 | // ---------
991 | // Sets a single Tocify option after the plugin is invoked
992 | setOption: function() {
993 |
994 | // Calls the jQueryUI Widget Factory setOption method
995 | $.Widget.prototype._setOption.apply(this, arguments);
996 |
997 | },
998 |
999 | // setOptions
1000 | // ----------
1001 | // Sets a single or multiple Tocify options after the plugin is invoked
1002 | setOptions: function() {
1003 |
1004 | // Calls the jQueryUI Widget Factory setOptions method
1005 | $.Widget.prototype._setOptions.apply(this, arguments);
1006 |
1007 | },
1008 |
1009 | // _scrollTo
1010 | // ---------
1011 | // Scrolls to a specific element
1012 | _scrollTo: function(elem) {
1013 |
1014 | var self = this,
1015 | duration = self.options.smoothScroll || 0,
1016 | scrollTo = self.options.scrollTo;
1017 |
1018 | // Once all animations on the page are complete, this callback function will be called
1019 | $("html, body").promise().done(function() {
1020 |
1021 | // Animates the html and body element scrolltops
1022 | $("html, body").animate({
1023 |
1024 | // Sets the jQuery `scrollTop` to the top offset of the HTML div tag that matches the current list item's `data-unique` tag
1025 | "scrollTop": $('div[data-unique="' + elem.attr("data-unique") + '"]').next().offset().top - ($.isFunction(scrollTo) ? scrollTo.call() : scrollTo) + "px"
1026 |
1027 | }, {
1028 |
1029 | // Sets the smoothScroll animation time duration to the smoothScrollSpeed option
1030 | "duration": duration
1031 |
1032 | });
1033 |
1034 | });
1035 |
1036 | // Maintains chainability
1037 | return self;
1038 |
1039 | }
1040 |
1041 | });
1042 |
1043 | })); //end of plugin
--------------------------------------------------------------------------------
/source/javascripts/lib/lunr.js:
--------------------------------------------------------------------------------
1 | /**
2 | * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 0.5.7
3 | * Copyright (C) 2014 Oliver Nightingale
4 | * MIT Licensed
5 | * @license
6 | */
7 |
8 | (function(){
9 |
10 | /**
11 | * Convenience function for instantiating a new lunr index and configuring it
12 | * with the default pipeline functions and the passed config function.
13 | *
14 | * When using this convenience function a new index will be created with the
15 | * following functions already in the pipeline:
16 | *
17 | * lunr.StopWordFilter - filters out any stop words before they enter the
18 | * index
19 | *
20 | * lunr.stemmer - stems the tokens before entering the index.
21 | *
22 | * Example:
23 | *
24 | * var idx = lunr(function () {
25 | * this.field('title', 10)
26 | * this.field('tags', 100)
27 | * this.field('body')
28 | *
29 | * this.ref('cid')
30 | *
31 | * this.pipeline.add(function () {
32 | * // some custom pipeline function
33 | * })
34 | *
35 | * })
36 | *
37 | * @param {Function} config A function that will be called with the new instance
38 | * of the lunr.Index as both its context and first parameter. It can be used to
39 | * customize the instance of new lunr.Index.
40 | * @namespace
41 | * @module
42 | * @returns {lunr.Index}
43 | *
44 | */
45 | var lunr = function (config) {
46 | var idx = new lunr.Index
47 |
48 | idx.pipeline.add(
49 | lunr.trimmer,
50 | lunr.stopWordFilter,
51 | lunr.stemmer
52 | )
53 |
54 | if (config) config.call(idx, idx)
55 |
56 | return idx
57 | }
58 |
59 | lunr.version = "0.5.7"
60 | /*!
61 | * lunr.utils
62 | * Copyright (C) 2014 Oliver Nightingale
63 | */
64 |
65 | /**
66 | * A namespace containing utils for the rest of the lunr library
67 | */
68 | lunr.utils = {}
69 |
70 | /**
71 | * Print a warning message to the console.
72 | *
73 | * @param {String} message The message to be printed.
74 | * @memberOf Utils
75 | */
76 | lunr.utils.warn = (function (global) {
77 | return function (message) {
78 | if (global.console && console.warn) {
79 | console.warn(message)
80 | }
81 | }
82 | })(this)
83 |
84 | /*!
85 | * lunr.EventEmitter
86 | * Copyright (C) 2014 Oliver Nightingale
87 | */
88 |
89 | /**
90 | * lunr.EventEmitter is an event emitter for lunr. It manages adding and removing event handlers and triggering events and their handlers.
91 | *
92 | * @constructor
93 | */
94 | lunr.EventEmitter = function () {
95 | this.events = {}
96 | }
97 |
98 | /**
99 | * Binds a handler function to a specific event(s).
100 | *
101 | * Can bind a single function to many different events in one call.
102 | *
103 | * @param {String} [eventName] The name(s) of events to bind this function to.
104 | * @param {Function} handler The function to call when an event is fired.
105 | * @memberOf EventEmitter
106 | */
107 | lunr.EventEmitter.prototype.addListener = function () {
108 | var args = Array.prototype.slice.call(arguments),
109 | fn = args.pop(),
110 | names = args
111 |
112 | if (typeof fn !== "function") throw new TypeError ("last argument must be a function")
113 |
114 | names.forEach(function (name) {
115 | if (!this.hasHandler(name)) this.events[name] = []
116 | this.events[name].push(fn)
117 | }, this)
118 | }
119 |
120 | /**
121 | * Removes a handler function from a specific event.
122 | *
123 | * @param {String} eventName The name of the event to remove this function from.
124 | * @param {Function} handler The function to remove from an event.
125 | * @memberOf EventEmitter
126 | */
127 | lunr.EventEmitter.prototype.removeListener = function (name, fn) {
128 | if (!this.hasHandler(name)) return
129 |
130 | var fnIndex = this.events[name].indexOf(fn)
131 | this.events[name].splice(fnIndex, 1)
132 |
133 | if (!this.events[name].length) delete this.events[name]
134 | }
135 |
136 | /**
137 | * Calls all functions bound to the given event.
138 | *
139 | * Additional data can be passed to the event handler as arguments to `emit`
140 | * after the event name.
141 | *
142 | * @param {String} eventName The name of the event to emit.
143 | * @memberOf EventEmitter
144 | */
145 | lunr.EventEmitter.prototype.emit = function (name) {
146 | if (!this.hasHandler(name)) return
147 |
148 | var args = Array.prototype.slice.call(arguments, 1)
149 |
150 | this.events[name].forEach(function (fn) {
151 | fn.apply(undefined, args)
152 | })
153 | }
154 |
155 | /**
156 | * Checks whether a handler has ever been stored against an event.
157 | *
158 | * @param {String} eventName The name of the event to check.
159 | * @private
160 | * @memberOf EventEmitter
161 | */
162 | lunr.EventEmitter.prototype.hasHandler = function (name) {
163 | return name in this.events
164 | }
165 |
166 | /*!
167 | * lunr.tokenizer
168 | * Copyright (C) 2014 Oliver Nightingale
169 | */
170 |
171 | /**
172 | * A function for splitting a string into tokens ready to be inserted into
173 | * the search index.
174 | *
175 | * @module
176 | * @param {String} obj The string to convert into tokens
177 | * @returns {Array}
178 | */
179 | lunr.tokenizer = function (obj) {
180 | if (!arguments.length || obj == null || obj == undefined) return []
181 | if (Array.isArray(obj)) return obj.map(function (t) { return t.toLowerCase() })
182 |
183 | var str = obj.toString().replace(/^\s+/, '')
184 |
185 | for (var i = str.length - 1; i >= 0; i--) {
186 | if (/\S/.test(str.charAt(i))) {
187 | str = str.substring(0, i + 1)
188 | break
189 | }
190 | }
191 |
192 | return str
193 | .split(/(?:\s+|\-)/)
194 | .filter(function (token) {
195 | return !!token
196 | })
197 | .map(function (token) {
198 | return token.toLowerCase()
199 | })
200 | }
201 | /*!
202 | * lunr.Pipeline
203 | * Copyright (C) 2014 Oliver Nightingale
204 | */
205 |
206 | /**
207 | * lunr.Pipelines maintain an ordered list of functions to be applied to all
208 | * tokens in documents entering the search index and queries being ran against
209 | * the index.
210 | *
211 | * An instance of lunr.Index created with the lunr shortcut will contain a
212 | * pipeline with a stop word filter and an English language stemmer. Extra
213 | * functions can be added before or after either of these functions or these
214 | * default functions can be removed.
215 | *
216 | * When run the pipeline will call each function in turn, passing a token, the
217 | * index of that token in the original list of all tokens and finally a list of
218 | * all the original tokens.
219 | *
220 | * The output of functions in the pipeline will be passed to the next function
221 | * in the pipeline. To exclude a token from entering the index the function
222 | * should return undefined, the rest of the pipeline will not be called with
223 | * this token.
224 | *
225 | * For serialisation of pipelines to work, all functions used in an instance of
226 | * a pipeline should be registered with lunr.Pipeline. Registered functions can
227 | * then be loaded. If trying to load a serialised pipeline that uses functions
228 | * that are not registered an error will be thrown.
229 | *
230 | * If not planning on serialising the pipeline then registering pipeline functions
231 | * is not necessary.
232 | *
233 | * @constructor
234 | */
235 | lunr.Pipeline = function () {
236 | this._stack = []
237 | }
238 |
239 | lunr.Pipeline.registeredFunctions = {}
240 |
241 | /**
242 | * Register a function with the pipeline.
243 | *
244 | * Functions that are used in the pipeline should be registered if the pipeline
245 | * needs to be serialised, or a serialised pipeline needs to be loaded.
246 | *
247 | * Registering a function does not add it to a pipeline, functions must still be
248 | * added to instances of the pipeline for them to be used when running a pipeline.
249 | *
250 | * @param {Function} fn The function to check for.
251 | * @param {String} label The label to register this function with
252 | * @memberOf Pipeline
253 | */
254 | lunr.Pipeline.registerFunction = function (fn, label) {
255 | if (label in this.registeredFunctions) {
256 | lunr.utils.warn('Overwriting existing registered function: ' + label)
257 | }
258 |
259 | fn.label = label
260 | lunr.Pipeline.registeredFunctions[fn.label] = fn
261 | }
262 |
263 | /**
264 | * Warns if the function is not registered as a Pipeline function.
265 | *
266 | * @param {Function} fn The function to check for.
267 | * @private
268 | * @memberOf Pipeline
269 | */
270 | lunr.Pipeline.warnIfFunctionNotRegistered = function (fn) {
271 | var isRegistered = fn.label && (fn.label in this.registeredFunctions)
272 |
273 | if (!isRegistered) {
274 | lunr.utils.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\n', fn)
275 | }
276 | }
277 |
278 | /**
279 | * Loads a previously serialised pipeline.
280 | *
281 | * All functions to be loaded must already be registered with lunr.Pipeline.
282 | * If any function from the serialised data has not been registered then an
283 | * error will be thrown.
284 | *
285 | * @param {Object} serialised The serialised pipeline to load.
286 | * @returns {lunr.Pipeline}
287 | * @memberOf Pipeline
288 | */
289 | lunr.Pipeline.load = function (serialised) {
290 | var pipeline = new lunr.Pipeline
291 |
292 | serialised.forEach(function (fnName) {
293 | var fn = lunr.Pipeline.registeredFunctions[fnName]
294 |
295 | if (fn) {
296 | pipeline.add(fn)
297 | } else {
298 | throw new Error ('Cannot load un-registered function: ' + fnName)
299 | }
300 | })
301 |
302 | return pipeline
303 | }
304 |
305 | /**
306 | * Adds new functions to the end of the pipeline.
307 | *
308 | * Logs a warning if the function has not been registered.
309 | *
310 | * @param {Function} functions Any number of functions to add to the pipeline.
311 | * @memberOf Pipeline
312 | */
313 | lunr.Pipeline.prototype.add = function () {
314 | var fns = Array.prototype.slice.call(arguments)
315 |
316 | fns.forEach(function (fn) {
317 | lunr.Pipeline.warnIfFunctionNotRegistered(fn)
318 | this._stack.push(fn)
319 | }, this)
320 | }
321 |
322 | /**
323 | * Adds a single function after a function that already exists in the
324 | * pipeline.
325 | *
326 | * Logs a warning if the function has not been registered.
327 | *
328 | * @param {Function} existingFn A function that already exists in the pipeline.
329 | * @param {Function} newFn The new function to add to the pipeline.
330 | * @memberOf Pipeline
331 | */
332 | lunr.Pipeline.prototype.after = function (existingFn, newFn) {
333 | lunr.Pipeline.warnIfFunctionNotRegistered(newFn)
334 |
335 | var pos = this._stack.indexOf(existingFn) + 1
336 | this._stack.splice(pos, 0, newFn)
337 | }
338 |
339 | /**
340 | * Adds a single function before a function that already exists in the
341 | * pipeline.
342 | *
343 | * Logs a warning if the function has not been registered.
344 | *
345 | * @param {Function} existingFn A function that already exists in the pipeline.
346 | * @param {Function} newFn The new function to add to the pipeline.
347 | * @memberOf Pipeline
348 | */
349 | lunr.Pipeline.prototype.before = function (existingFn, newFn) {
350 | lunr.Pipeline.warnIfFunctionNotRegistered(newFn)
351 |
352 | var pos = this._stack.indexOf(existingFn)
353 | this._stack.splice(pos, 0, newFn)
354 | }
355 |
356 | /**
357 | * Removes a function from the pipeline.
358 | *
359 | * @param {Function} fn The function to remove from the pipeline.
360 | * @memberOf Pipeline
361 | */
362 | lunr.Pipeline.prototype.remove = function (fn) {
363 | var pos = this._stack.indexOf(fn)
364 | this._stack.splice(pos, 1)
365 | }
366 |
367 | /**
368 | * Runs the current list of functions that make up the pipeline against the
369 | * passed tokens.
370 | *
371 | * @param {Array} tokens The tokens to run through the pipeline.
372 | * @returns {Array}
373 | * @memberOf Pipeline
374 | */
375 | lunr.Pipeline.prototype.run = function (tokens) {
376 | var out = [],
377 | tokenLength = tokens.length,
378 | stackLength = this._stack.length
379 |
380 | for (var i = 0; i < tokenLength; i++) {
381 | var token = tokens[i]
382 |
383 | for (var j = 0; j < stackLength; j++) {
384 | token = this._stack[j](token, i, tokens)
385 | if (token === void 0) break
386 | };
387 |
388 | if (token !== void 0) out.push(token)
389 | };
390 |
391 | return out
392 | }
393 |
394 | /**
395 | * Resets the pipeline by removing any existing processors.
396 | *
397 | * @memberOf Pipeline
398 | */
399 | lunr.Pipeline.prototype.reset = function () {
400 | this._stack = []
401 | }
402 |
403 | /**
404 | * Returns a representation of the pipeline ready for serialisation.
405 | *
406 | * Logs a warning if the function has not been registered.
407 | *
408 | * @returns {Array}
409 | * @memberOf Pipeline
410 | */
411 | lunr.Pipeline.prototype.toJSON = function () {
412 | return this._stack.map(function (fn) {
413 | lunr.Pipeline.warnIfFunctionNotRegistered(fn)
414 |
415 | return fn.label
416 | })
417 | }
418 | /*!
419 | * lunr.Vector
420 | * Copyright (C) 2014 Oliver Nightingale
421 | */
422 |
423 | /**
424 | * lunr.Vectors implement vector related operations for
425 | * a series of elements.
426 | *
427 | * @constructor
428 | */
429 | lunr.Vector = function () {
430 | this._magnitude = null
431 | this.list = undefined
432 | this.length = 0
433 | }
434 |
435 | /**
436 | * lunr.Vector.Node is a simple struct for each node
437 | * in a lunr.Vector.
438 | *
439 | * @private
440 | * @param {Number} The index of the node in the vector.
441 | * @param {Object} The data at this node in the vector.
442 | * @param {lunr.Vector.Node} The node directly after this node in the vector.
443 | * @constructor
444 | * @memberOf Vector
445 | */
446 | lunr.Vector.Node = function (idx, val, next) {
447 | this.idx = idx
448 | this.val = val
449 | this.next = next
450 | }
451 |
452 | /**
453 | * Inserts a new value at a position in a vector.
454 | *
455 | * @param {Number} The index at which to insert a value.
456 | * @param {Object} The object to insert in the vector.
457 | * @memberOf Vector.
458 | */
459 | lunr.Vector.prototype.insert = function (idx, val) {
460 | var list = this.list
461 |
462 | if (!list) {
463 | this.list = new lunr.Vector.Node (idx, val, list)
464 | return this.length++
465 | }
466 |
467 | var prev = list,
468 | next = list.next
469 |
470 | while (next != undefined) {
471 | if (idx < next.idx) {
472 | prev.next = new lunr.Vector.Node (idx, val, next)
473 | return this.length++
474 | }
475 |
476 | prev = next, next = next.next
477 | }
478 |
479 | prev.next = new lunr.Vector.Node (idx, val, next)
480 | return this.length++
481 | }
482 |
483 | /**
484 | * Calculates the magnitude of this vector.
485 | *
486 | * @returns {Number}
487 | * @memberOf Vector
488 | */
489 | lunr.Vector.prototype.magnitude = function () {
490 | if (this._magniture) return this._magnitude
491 | var node = this.list,
492 | sumOfSquares = 0,
493 | val
494 |
495 | while (node) {
496 | val = node.val
497 | sumOfSquares += val * val
498 | node = node.next
499 | }
500 |
501 | return this._magnitude = Math.sqrt(sumOfSquares)
502 | }
503 |
504 | /**
505 | * Calculates the dot product of this vector and another vector.
506 | *
507 | * @param {lunr.Vector} otherVector The vector to compute the dot product with.
508 | * @returns {Number}
509 | * @memberOf Vector
510 | */
511 | lunr.Vector.prototype.dot = function (otherVector) {
512 | var node = this.list,
513 | otherNode = otherVector.list,
514 | dotProduct = 0
515 |
516 | while (node && otherNode) {
517 | if (node.idx < otherNode.idx) {
518 | node = node.next
519 | } else if (node.idx > otherNode.idx) {
520 | otherNode = otherNode.next
521 | } else {
522 | dotProduct += node.val * otherNode.val
523 | node = node.next
524 | otherNode = otherNode.next
525 | }
526 | }
527 |
528 | return dotProduct
529 | }
530 |
531 | /**
532 | * Calculates the cosine similarity between this vector and another
533 | * vector.
534 | *
535 | * @param {lunr.Vector} otherVector The other vector to calculate the
536 | * similarity with.
537 | * @returns {Number}
538 | * @memberOf Vector
539 | */
540 | lunr.Vector.prototype.similarity = function (otherVector) {
541 | return this.dot(otherVector) / (this.magnitude() * otherVector.magnitude())
542 | }
543 | /*!
544 | * lunr.SortedSet
545 | * Copyright (C) 2014 Oliver Nightingale
546 | */
547 |
548 | /**
549 | * lunr.SortedSets are used to maintain an array of uniq values in a sorted
550 | * order.
551 | *
552 | * @constructor
553 | */
554 | lunr.SortedSet = function () {
555 | this.length = 0
556 | this.elements = []
557 | }
558 |
559 | /**
560 | * Loads a previously serialised sorted set.
561 | *
562 | * @param {Array} serialisedData The serialised set to load.
563 | * @returns {lunr.SortedSet}
564 | * @memberOf SortedSet
565 | */
566 | lunr.SortedSet.load = function (serialisedData) {
567 | var set = new this
568 |
569 | set.elements = serialisedData
570 | set.length = serialisedData.length
571 |
572 | return set
573 | }
574 |
575 | /**
576 | * Inserts new items into the set in the correct position to maintain the
577 | * order.
578 | *
579 | * @param {Object} The objects to add to this set.
580 | * @memberOf SortedSet
581 | */
582 | lunr.SortedSet.prototype.add = function () {
583 | Array.prototype.slice.call(arguments).forEach(function (element) {
584 | if (~this.indexOf(element)) return
585 | this.elements.splice(this.locationFor(element), 0, element)
586 | }, this)
587 |
588 | this.length = this.elements.length
589 | }
590 |
591 | /**
592 | * Converts this sorted set into an array.
593 | *
594 | * @returns {Array}
595 | * @memberOf SortedSet
596 | */
597 | lunr.SortedSet.prototype.toArray = function () {
598 | return this.elements.slice()
599 | }
600 |
601 | /**
602 | * Creates a new array with the results of calling a provided function on every
603 | * element in this sorted set.
604 | *
605 | * Delegates to Array.prototype.map and has the same signature.
606 | *
607 | * @param {Function} fn The function that is called on each element of the
608 | * set.
609 | * @param {Object} ctx An optional object that can be used as the context
610 | * for the function fn.
611 | * @returns {Array}
612 | * @memberOf SortedSet
613 | */
614 | lunr.SortedSet.prototype.map = function (fn, ctx) {
615 | return this.elements.map(fn, ctx)
616 | }
617 |
618 | /**
619 | * Executes a provided function once per sorted set element.
620 | *
621 | * Delegates to Array.prototype.forEach and has the same signature.
622 | *
623 | * @param {Function} fn The function that is called on each element of the
624 | * set.
625 | * @param {Object} ctx An optional object that can be used as the context
626 | * @memberOf SortedSet
627 | * for the function fn.
628 | */
629 | lunr.SortedSet.prototype.forEach = function (fn, ctx) {
630 | return this.elements.forEach(fn, ctx)
631 | }
632 |
633 | /**
634 | * Returns the index at which a given element can be found in the
635 | * sorted set, or -1 if it is not present.
636 | *
637 | * @param {Object} elem The object to locate in the sorted set.
638 | * @param {Number} start An optional index at which to start searching from
639 | * within the set.
640 | * @param {Number} end An optional index at which to stop search from within
641 | * the set.
642 | * @returns {Number}
643 | * @memberOf SortedSet
644 | */
645 | lunr.SortedSet.prototype.indexOf = function (elem, start, end) {
646 | var start = start || 0,
647 | end = end || this.elements.length,
648 | sectionLength = end - start,
649 | pivot = start + Math.floor(sectionLength / 2),
650 | pivotElem = this.elements[pivot]
651 |
652 | if (sectionLength <= 1) {
653 | if (pivotElem === elem) {
654 | return pivot
655 | } else {
656 | return -1
657 | }
658 | }
659 |
660 | if (pivotElem < elem) return this.indexOf(elem, pivot, end)
661 | if (pivotElem > elem) return this.indexOf(elem, start, pivot)
662 | if (pivotElem === elem) return pivot
663 | }
664 |
665 | /**
666 | * Returns the position within the sorted set that an element should be
667 | * inserted at to maintain the current order of the set.
668 | *
669 | * This function assumes that the element to search for does not already exist
670 | * in the sorted set.
671 | *
672 | * @param {Object} elem The elem to find the position for in the set
673 | * @param {Number} start An optional index at which to start searching from
674 | * within the set.
675 | * @param {Number} end An optional index at which to stop search from within
676 | * the set.
677 | * @returns {Number}
678 | * @memberOf SortedSet
679 | */
680 | lunr.SortedSet.prototype.locationFor = function (elem, start, end) {
681 | var start = start || 0,
682 | end = end || this.elements.length,
683 | sectionLength = end - start,
684 | pivot = start + Math.floor(sectionLength / 2),
685 | pivotElem = this.elements[pivot]
686 |
687 | if (sectionLength <= 1) {
688 | if (pivotElem > elem) return pivot
689 | if (pivotElem < elem) return pivot + 1
690 | }
691 |
692 | if (pivotElem < elem) return this.locationFor(elem, pivot, end)
693 | if (pivotElem > elem) return this.locationFor(elem, start, pivot)
694 | }
695 |
696 | /**
697 | * Creates a new lunr.SortedSet that contains the elements in the intersection
698 | * of this set and the passed set.
699 | *
700 | * @param {lunr.SortedSet} otherSet The set to intersect with this set.
701 | * @returns {lunr.SortedSet}
702 | * @memberOf SortedSet
703 | */
704 | lunr.SortedSet.prototype.intersect = function (otherSet) {
705 | var intersectSet = new lunr.SortedSet,
706 | i = 0, j = 0,
707 | a_len = this.length, b_len = otherSet.length,
708 | a = this.elements, b = otherSet.elements
709 |
710 | while (true) {
711 | if (i > a_len - 1 || j > b_len - 1) break
712 |
713 | if (a[i] === b[j]) {
714 | intersectSet.add(a[i])
715 | i++, j++
716 | continue
717 | }
718 |
719 | if (a[i] < b[j]) {
720 | i++
721 | continue
722 | }
723 |
724 | if (a[i] > b[j]) {
725 | j++
726 | continue
727 | }
728 | };
729 |
730 | return intersectSet
731 | }
732 |
733 | /**
734 | * Makes a copy of this set
735 | *
736 | * @returns {lunr.SortedSet}
737 | * @memberOf SortedSet
738 | */
739 | lunr.SortedSet.prototype.clone = function () {
740 | var clone = new lunr.SortedSet
741 |
742 | clone.elements = this.toArray()
743 | clone.length = clone.elements.length
744 |
745 | return clone
746 | }
747 |
748 | /**
749 | * Creates a new lunr.SortedSet that contains the elements in the union
750 | * of this set and the passed set.
751 | *
752 | * @param {lunr.SortedSet} otherSet The set to union with this set.
753 | * @returns {lunr.SortedSet}
754 | * @memberOf SortedSet
755 | */
756 | lunr.SortedSet.prototype.union = function (otherSet) {
757 | var longSet, shortSet, unionSet
758 |
759 | if (this.length >= otherSet.length) {
760 | longSet = this, shortSet = otherSet
761 | } else {
762 | longSet = otherSet, shortSet = this
763 | }
764 |
765 | unionSet = longSet.clone()
766 |
767 | unionSet.add.apply(unionSet, shortSet.toArray())
768 |
769 | return unionSet
770 | }
771 |
772 | /**
773 | * Returns a representation of the sorted set ready for serialisation.
774 | *
775 | * @returns {Array}
776 | * @memberOf SortedSet
777 | */
778 | lunr.SortedSet.prototype.toJSON = function () {
779 | return this.toArray()
780 | }
781 | /*!
782 | * lunr.Index
783 | * Copyright (C) 2014 Oliver Nightingale
784 | */
785 |
786 | /**
787 | * lunr.Index is object that manages a search index. It contains the indexes
788 | * and stores all the tokens and document lookups. It also provides the main
789 | * user facing API for the library.
790 | *
791 | * @constructor
792 | */
793 | lunr.Index = function () {
794 | this._fields = []
795 | this._ref = 'id'
796 | this.pipeline = new lunr.Pipeline
797 | this.documentStore = new lunr.Store
798 | this.tokenStore = new lunr.TokenStore
799 | this.corpusTokens = new lunr.SortedSet
800 | this.eventEmitter = new lunr.EventEmitter
801 |
802 | this._idfCache = {}
803 |
804 | this.on('add', 'remove', 'update', (function () {
805 | this._idfCache = {}
806 | }).bind(this))
807 | }
808 |
809 | /**
810 | * Bind a handler to events being emitted by the index.
811 | *
812 | * The handler can be bound to many events at the same time.
813 | *
814 | * @param {String} [eventName] The name(s) of events to bind the function to.
815 | * @param {Function} handler The serialised set to load.
816 | * @memberOf Index
817 | */
818 | lunr.Index.prototype.on = function () {
819 | var args = Array.prototype.slice.call(arguments)
820 | return this.eventEmitter.addListener.apply(this.eventEmitter, args)
821 | }
822 |
823 | /**
824 | * Removes a handler from an event being emitted by the index.
825 | *
826 | * @param {String} eventName The name of events to remove the function from.
827 | * @param {Function} handler The serialised set to load.
828 | * @memberOf Index
829 | */
830 | lunr.Index.prototype.off = function (name, fn) {
831 | return this.eventEmitter.removeListener(name, fn)
832 | }
833 |
834 | /**
835 | * Loads a previously serialised index.
836 | *
837 | * Issues a warning if the index being imported was serialised
838 | * by a different version of lunr.
839 | *
840 | * @param {Object} serialisedData The serialised set to load.
841 | * @returns {lunr.Index}
842 | * @memberOf Index
843 | */
844 | lunr.Index.load = function (serialisedData) {
845 | if (serialisedData.version !== lunr.version) {
846 | lunr.utils.warn('version mismatch: current ' + lunr.version + ' importing ' + serialisedData.version)
847 | }
848 |
849 | var idx = new this
850 |
851 | idx._fields = serialisedData.fields
852 | idx._ref = serialisedData.ref
853 |
854 | idx.documentStore = lunr.Store.load(serialisedData.documentStore)
855 | idx.tokenStore = lunr.TokenStore.load(serialisedData.tokenStore)
856 | idx.corpusTokens = lunr.SortedSet.load(serialisedData.corpusTokens)
857 | idx.pipeline = lunr.Pipeline.load(serialisedData.pipeline)
858 |
859 | return idx
860 | }
861 |
862 | /**
863 | * Adds a field to the list of fields that will be searchable within documents
864 | * in the index.
865 | *
866 | * An optional boost param can be passed to affect how much tokens in this field
867 | * rank in search results, by default the boost value is 1.
868 | *
869 | * Fields should be added before any documents are added to the index, fields
870 | * that are added after documents are added to the index will only apply to new
871 | * documents added to the index.
872 | *
873 | * @param {String} fieldName The name of the field within the document that
874 | * should be indexed
875 | * @param {Number} boost An optional boost that can be applied to terms in this
876 | * field.
877 | * @returns {lunr.Index}
878 | * @memberOf Index
879 | */
880 | lunr.Index.prototype.field = function (fieldName, opts) {
881 | var opts = opts || {},
882 | field = { name: fieldName, boost: opts.boost || 1 }
883 |
884 | this._fields.push(field)
885 | return this
886 | }
887 |
888 | /**
889 | * Sets the property used to uniquely identify documents added to the index,
890 | * by default this property is 'id'.
891 | *
892 | * This should only be changed before adding documents to the index, changing
893 | * the ref property without resetting the index can lead to unexpected results.
894 | *
895 | * @param {String} refName The property to use to uniquely identify the
896 | * documents in the index.
897 | * @param {Boolean} emitEvent Whether to emit add events, defaults to true
898 | * @returns {lunr.Index}
899 | * @memberOf Index
900 | */
901 | lunr.Index.prototype.ref = function (refName) {
902 | this._ref = refName
903 | return this
904 | }
905 |
906 | /**
907 | * Add a document to the index.
908 | *
909 | * This is the way new documents enter the index, this function will run the
910 | * fields from the document through the index's pipeline and then add it to
911 | * the index, it will then show up in search results.
912 | *
913 | * An 'add' event is emitted with the document that has been added and the index
914 | * the document has been added to. This event can be silenced by passing false
915 | * as the second argument to add.
916 | *
917 | * @param {Object} doc The document to add to the index.
918 | * @param {Boolean} emitEvent Whether or not to emit events, default true.
919 | * @memberOf Index
920 | */
921 | lunr.Index.prototype.add = function (doc, emitEvent) {
922 | var docTokens = {},
923 | allDocumentTokens = new lunr.SortedSet,
924 | docRef = doc[this._ref],
925 | emitEvent = emitEvent === undefined ? true : emitEvent
926 |
927 | this._fields.forEach(function (field) {
928 | var fieldTokens = this.pipeline.run(lunr.tokenizer(doc[field.name]))
929 |
930 | docTokens[field.name] = fieldTokens
931 | lunr.SortedSet.prototype.add.apply(allDocumentTokens, fieldTokens)
932 | }, this)
933 |
934 | this.documentStore.set(docRef, allDocumentTokens)
935 | lunr.SortedSet.prototype.add.apply(this.corpusTokens, allDocumentTokens.toArray())
936 |
937 | for (var i = 0; i < allDocumentTokens.length; i++) {
938 | var token = allDocumentTokens.elements[i]
939 | var tf = this._fields.reduce(function (memo, field) {
940 | var fieldLength = docTokens[field.name].length
941 |
942 | if (!fieldLength) return memo
943 |
944 | var tokenCount = docTokens[field.name].filter(function (t) { return t === token }).length
945 |
946 | return memo + (tokenCount / fieldLength * field.boost)
947 | }, 0)
948 |
949 | this.tokenStore.add(token, { ref: docRef, tf: tf })
950 | };
951 |
952 | if (emitEvent) this.eventEmitter.emit('add', doc, this)
953 | }
954 |
955 | /**
956 | * Removes a document from the index.
957 | *
958 | * To make sure documents no longer show up in search results they can be
959 | * removed from the index using this method.
960 | *
961 | * The document passed only needs to have the same ref property value as the
962 | * document that was added to the index, they could be completely different
963 | * objects.
964 | *
965 | * A 'remove' event is emitted with the document that has been removed and the index
966 | * the document has been removed from. This event can be silenced by passing false
967 | * as the second argument to remove.
968 | *
969 | * @param {Object} doc The document to remove from the index.
970 | * @param {Boolean} emitEvent Whether to emit remove events, defaults to true
971 | * @memberOf Index
972 | */
973 | lunr.Index.prototype.remove = function (doc, emitEvent) {
974 | var docRef = doc[this._ref],
975 | emitEvent = emitEvent === undefined ? true : emitEvent
976 |
977 | if (!this.documentStore.has(docRef)) return
978 |
979 | var docTokens = this.documentStore.get(docRef)
980 |
981 | this.documentStore.remove(docRef)
982 |
983 | docTokens.forEach(function (token) {
984 | this.tokenStore.remove(token, docRef)
985 | }, this)
986 |
987 | if (emitEvent) this.eventEmitter.emit('remove', doc, this)
988 | }
989 |
990 | /**
991 | * Updates a document in the index.
992 | *
993 | * When a document contained within the index gets updated, fields changed,
994 | * added or removed, to make sure it correctly matched against search queries,
995 | * it should be updated in the index.
996 | *
997 | * This method is just a wrapper around `remove` and `add`
998 | *
999 | * An 'update' event is emitted with the document that has been updated and the index.
1000 | * This event can be silenced by passing false as the second argument to update. Only
1001 | * an update event will be fired, the 'add' and 'remove' events of the underlying calls
1002 | * are silenced.
1003 | *
1004 | * @param {Object} doc The document to update in the index.
1005 | * @param {Boolean} emitEvent Whether to emit update events, defaults to true
1006 | * @see Index.prototype.remove
1007 | * @see Index.prototype.add
1008 | * @memberOf Index
1009 | */
1010 | lunr.Index.prototype.update = function (doc, emitEvent) {
1011 | var emitEvent = emitEvent === undefined ? true : emitEvent
1012 |
1013 | this.remove(doc, false)
1014 | this.add(doc, false)
1015 |
1016 | if (emitEvent) this.eventEmitter.emit('update', doc, this)
1017 | }
1018 |
1019 | /**
1020 | * Calculates the inverse document frequency for a token within the index.
1021 | *
1022 | * @param {String} token The token to calculate the idf of.
1023 | * @see Index.prototype.idf
1024 | * @private
1025 | * @memberOf Index
1026 | */
1027 | lunr.Index.prototype.idf = function (term) {
1028 | var cacheKey = "@" + term
1029 | if (Object.prototype.hasOwnProperty.call(this._idfCache, cacheKey)) return this._idfCache[cacheKey]
1030 |
1031 | var documentFrequency = this.tokenStore.count(term),
1032 | idf = 1
1033 |
1034 | if (documentFrequency > 0) {
1035 | idf = 1 + Math.log(this.tokenStore.length / documentFrequency)
1036 | }
1037 |
1038 | return this._idfCache[cacheKey] = idf
1039 | }
1040 |
1041 | /**
1042 | * Searches the index using the passed query.
1043 | *
1044 | * Queries should be a string, multiple words are allowed and will lead to an
1045 | * AND based query, e.g. `idx.search('foo bar')` will run a search for
1046 | * documents containing both 'foo' and 'bar'.
1047 | *
1048 | * All query tokens are passed through the same pipeline that document tokens
1049 | * are passed through, so any language processing involved will be run on every
1050 | * query term.
1051 | *
1052 | * Each query term is expanded, so that the term 'he' might be expanded to
1053 | * 'hello' and 'help' if those terms were already included in the index.
1054 | *
1055 | * Matching documents are returned as an array of objects, each object contains
1056 | * the matching document ref, as set for this index, and the similarity score
1057 | * for this document against the query.
1058 | *
1059 | * @param {String} query The query to search the index with.
1060 | * @returns {Object}
1061 | * @see Index.prototype.idf
1062 | * @see Index.prototype.documentVector
1063 | * @memberOf Index
1064 | */
1065 | lunr.Index.prototype.search = function (query) {
1066 | var queryTokens = this.pipeline.run(lunr.tokenizer(query)),
1067 | queryVector = new lunr.Vector,
1068 | documentSets = [],
1069 | fieldBoosts = this._fields.reduce(function (memo, f) { return memo + f.boost }, 0)
1070 |
1071 | var hasSomeToken = queryTokens.some(function (token) {
1072 | return this.tokenStore.has(token)
1073 | }, this)
1074 |
1075 | if (!hasSomeToken) return []
1076 |
1077 | queryTokens
1078 | .forEach(function (token, i, tokens) {
1079 | var tf = 1 / tokens.length * this._fields.length * fieldBoosts,
1080 | self = this
1081 |
1082 | var set = this.tokenStore.expand(token).reduce(function (memo, key) {
1083 | var pos = self.corpusTokens.indexOf(key),
1084 | idf = self.idf(key),
1085 | similarityBoost = 1,
1086 | set = new lunr.SortedSet
1087 |
1088 | // if the expanded key is not an exact match to the token then
1089 | // penalise the score for this key by how different the key is
1090 | // to the token.
1091 | if (key !== token) {
1092 | var diff = Math.max(3, key.length - token.length)
1093 | similarityBoost = 1 / Math.log(diff)
1094 | }
1095 |
1096 | // calculate the query tf-idf score for this token
1097 | // applying an similarityBoost to ensure exact matches
1098 | // these rank higher than expanded terms
1099 | if (pos > -1) queryVector.insert(pos, tf * idf * similarityBoost)
1100 |
1101 | // add all the documents that have this key into a set
1102 | Object.keys(self.tokenStore.get(key)).forEach(function (ref) { set.add(ref) })
1103 |
1104 | return memo.union(set)
1105 | }, new lunr.SortedSet)
1106 |
1107 | documentSets.push(set)
1108 | }, this)
1109 |
1110 | var documentSet = documentSets.reduce(function (memo, set) {
1111 | return memo.intersect(set)
1112 | })
1113 |
1114 | return documentSet
1115 | .map(function (ref) {
1116 | return { ref: ref, score: queryVector.similarity(this.documentVector(ref)) }
1117 | }, this)
1118 | .sort(function (a, b) {
1119 | return b.score - a.score
1120 | })
1121 | }
1122 |
1123 | /**
1124 | * Generates a vector containing all the tokens in the document matching the
1125 | * passed documentRef.
1126 | *
1127 | * The vector contains the tf-idf score for each token contained in the
1128 | * document with the passed documentRef. The vector will contain an element
1129 | * for every token in the indexes corpus, if the document does not contain that
1130 | * token the element will be 0.
1131 | *
1132 | * @param {Object} documentRef The ref to find the document with.
1133 | * @returns {lunr.Vector}
1134 | * @private
1135 | * @memberOf Index
1136 | */
1137 | lunr.Index.prototype.documentVector = function (documentRef) {
1138 | var documentTokens = this.documentStore.get(documentRef),
1139 | documentTokensLength = documentTokens.length,
1140 | documentVector = new lunr.Vector
1141 |
1142 | for (var i = 0; i < documentTokensLength; i++) {
1143 | var token = documentTokens.elements[i],
1144 | tf = this.tokenStore.get(token)[documentRef].tf,
1145 | idf = this.idf(token)
1146 |
1147 | documentVector.insert(this.corpusTokens.indexOf(token), tf * idf)
1148 | };
1149 |
1150 | return documentVector
1151 | }
1152 |
1153 | /**
1154 | * Returns a representation of the index ready for serialisation.
1155 | *
1156 | * @returns {Object}
1157 | * @memberOf Index
1158 | */
1159 | lunr.Index.prototype.toJSON = function () {
1160 | return {
1161 | version: lunr.version,
1162 | fields: this._fields,
1163 | ref: this._ref,
1164 | documentStore: this.documentStore.toJSON(),
1165 | tokenStore: this.tokenStore.toJSON(),
1166 | corpusTokens: this.corpusTokens.toJSON(),
1167 | pipeline: this.pipeline.toJSON()
1168 | }
1169 | }
1170 |
1171 | /**
1172 | * Applies a plugin to the current index.
1173 | *
1174 | * A plugin is a function that is called with the index as its context.
1175 | * Plugins can be used to customise or extend the behaviour the index
1176 | * in some way. A plugin is just a function, that encapsulated the custom
1177 | * behaviour that should be applied to the index.
1178 | *
1179 | * The plugin function will be called with the index as its argument, additional
1180 | * arguments can also be passed when calling use. The function will be called
1181 | * with the index as its context.
1182 | *
1183 | * Example:
1184 | *
1185 | * var myPlugin = function (idx, arg1, arg2) {
1186 | * // `this` is the index to be extended
1187 | * // apply any extensions etc here.
1188 | * }
1189 | *
1190 | * var idx = lunr(function () {
1191 | * this.use(myPlugin, 'arg1', 'arg2')
1192 | * })
1193 | *
1194 | * @param {Function} plugin The plugin to apply.
1195 | * @memberOf Index
1196 | */
1197 | lunr.Index.prototype.use = function (plugin) {
1198 | var args = Array.prototype.slice.call(arguments, 1)
1199 | args.unshift(this)
1200 | plugin.apply(this, args)
1201 | }
1202 | /*!
1203 | * lunr.Store
1204 | * Copyright (C) 2014 Oliver Nightingale
1205 | */
1206 |
1207 | /**
1208 | * lunr.Store is a simple key-value store used for storing sets of tokens for
1209 | * documents stored in index.
1210 | *
1211 | * @constructor
1212 | * @module
1213 | */
1214 | lunr.Store = function () {
1215 | this.store = {}
1216 | this.length = 0
1217 | }
1218 |
1219 | /**
1220 | * Loads a previously serialised store
1221 | *
1222 | * @param {Object} serialisedData The serialised store to load.
1223 | * @returns {lunr.Store}
1224 | * @memberOf Store
1225 | */
1226 | lunr.Store.load = function (serialisedData) {
1227 | var store = new this
1228 |
1229 | store.length = serialisedData.length
1230 | store.store = Object.keys(serialisedData.store).reduce(function (memo, key) {
1231 | memo[key] = lunr.SortedSet.load(serialisedData.store[key])
1232 | return memo
1233 | }, {})
1234 |
1235 | return store
1236 | }
1237 |
1238 | /**
1239 | * Stores the given tokens in the store against the given id.
1240 | *
1241 | * @param {Object} id The key used to store the tokens against.
1242 | * @param {Object} tokens The tokens to store against the key.
1243 | * @memberOf Store
1244 | */
1245 | lunr.Store.prototype.set = function (id, tokens) {
1246 | if (!this.has(id)) this.length++
1247 | this.store[id] = tokens
1248 | }
1249 |
1250 | /**
1251 | * Retrieves the tokens from the store for a given key.
1252 | *
1253 | * @param {Object} id The key to lookup and retrieve from the store.
1254 | * @returns {Object}
1255 | * @memberOf Store
1256 | */
1257 | lunr.Store.prototype.get = function (id) {
1258 | return this.store[id]
1259 | }
1260 |
1261 | /**
1262 | * Checks whether the store contains a key.
1263 | *
1264 | * @param {Object} id The id to look up in the store.
1265 | * @returns {Boolean}
1266 | * @memberOf Store
1267 | */
1268 | lunr.Store.prototype.has = function (id) {
1269 | return id in this.store
1270 | }
1271 |
1272 | /**
1273 | * Removes the value for a key in the store.
1274 | *
1275 | * @param {Object} id The id to remove from the store.
1276 | * @memberOf Store
1277 | */
1278 | lunr.Store.prototype.remove = function (id) {
1279 | if (!this.has(id)) return
1280 |
1281 | delete this.store[id]
1282 | this.length--
1283 | }
1284 |
1285 | /**
1286 | * Returns a representation of the store ready for serialisation.
1287 | *
1288 | * @returns {Object}
1289 | * @memberOf Store
1290 | */
1291 | lunr.Store.prototype.toJSON = function () {
1292 | return {
1293 | store: this.store,
1294 | length: this.length
1295 | }
1296 | }
1297 |
1298 | /*!
1299 | * lunr.stemmer
1300 | * Copyright (C) 2014 Oliver Nightingale
1301 | * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt
1302 | */
1303 |
1304 | /**
1305 | * lunr.stemmer is an english language stemmer, this is a JavaScript
1306 | * implementation of the PorterStemmer taken from http://tartaurs.org/~martin
1307 | *
1308 | * @module
1309 | * @param {String} str The string to stem
1310 | * @returns {String}
1311 | * @see lunr.Pipeline
1312 | */
1313 | lunr.stemmer = (function(){
1314 | var step2list = {
1315 | "ational" : "ate",
1316 | "tional" : "tion",
1317 | "enci" : "ence",
1318 | "anci" : "ance",
1319 | "izer" : "ize",
1320 | "bli" : "ble",
1321 | "alli" : "al",
1322 | "entli" : "ent",
1323 | "eli" : "e",
1324 | "ousli" : "ous",
1325 | "ization" : "ize",
1326 | "ation" : "ate",
1327 | "ator" : "ate",
1328 | "alism" : "al",
1329 | "iveness" : "ive",
1330 | "fulness" : "ful",
1331 | "ousness" : "ous",
1332 | "aliti" : "al",
1333 | "iviti" : "ive",
1334 | "biliti" : "ble",
1335 | "logi" : "log"
1336 | },
1337 |
1338 | step3list = {
1339 | "icate" : "ic",
1340 | "ative" : "",
1341 | "alize" : "al",
1342 | "iciti" : "ic",
1343 | "ical" : "ic",
1344 | "ful" : "",
1345 | "ness" : ""
1346 | },
1347 |
1348 | c = "[^aeiou]", // consonant
1349 | v = "[aeiouy]", // vowel
1350 | C = c + "[^aeiouy]*", // consonant sequence
1351 | V = v + "[aeiou]*", // vowel sequence
1352 |
1353 | mgr0 = "^(" + C + ")?" + V + C, // [C]VC... is m>0
1354 | meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$", // [C]VC[V] is m=1
1355 | mgr1 = "^(" + C + ")?" + V + C + V + C, // [C]VCVC... is m>1
1356 | s_v = "^(" + C + ")?" + v; // vowel in stem
1357 |
1358 | var re_mgr0 = new RegExp(mgr0);
1359 | var re_mgr1 = new RegExp(mgr1);
1360 | var re_meq1 = new RegExp(meq1);
1361 | var re_s_v = new RegExp(s_v);
1362 |
1363 | var re_1a = /^(.+?)(ss|i)es$/;
1364 | var re2_1a = /^(.+?)([^s])s$/;
1365 | var re_1b = /^(.+?)eed$/;
1366 | var re2_1b = /^(.+?)(ed|ing)$/;
1367 | var re_1b_2 = /.$/;
1368 | var re2_1b_2 = /(at|bl|iz)$/;
1369 | var re3_1b_2 = new RegExp("([^aeiouylsz])\\1$");
1370 | var re4_1b_2 = new RegExp("^" + C + v + "[^aeiouwxy]$");
1371 |
1372 | var re_1c = /^(.+?[^aeiou])y$/;
1373 | var re_2 = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;
1374 |
1375 | var re_3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;
1376 |
1377 | var re_4 = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;
1378 | var re2_4 = /^(.+?)(s|t)(ion)$/;
1379 |
1380 | var re_5 = /^(.+?)e$/;
1381 | var re_5_1 = /ll$/;
1382 | var re3_5 = new RegExp("^" + C + v + "[^aeiouwxy]$");
1383 |
1384 | var porterStemmer = function porterStemmer(w) {
1385 | var stem,
1386 | suffix,
1387 | firstch,
1388 | re,
1389 | re2,
1390 | re3,
1391 | re4;
1392 |
1393 | if (w.length < 3) { return w; }
1394 |
1395 | firstch = w.substr(0,1);
1396 | if (firstch == "y") {
1397 | w = firstch.toUpperCase() + w.substr(1);
1398 | }
1399 |
1400 | // Step 1a
1401 | re = re_1a
1402 | re2 = re2_1a;
1403 |
1404 | if (re.test(w)) { w = w.replace(re,"$1$2"); }
1405 | else if (re2.test(w)) { w = w.replace(re2,"$1$2"); }
1406 |
1407 | // Step 1b
1408 | re = re_1b;
1409 | re2 = re2_1b;
1410 | if (re.test(w)) {
1411 | var fp = re.exec(w);
1412 | re = re_mgr0;
1413 | if (re.test(fp[1])) {
1414 | re = re_1b_2;
1415 | w = w.replace(re,"");
1416 | }
1417 | } else if (re2.test(w)) {
1418 | var fp = re2.exec(w);
1419 | stem = fp[1];
1420 | re2 = re_s_v;
1421 | if (re2.test(stem)) {
1422 | w = stem;
1423 | re2 = re2_1b_2;
1424 | re3 = re3_1b_2;
1425 | re4 = re4_1b_2;
1426 | if (re2.test(w)) { w = w + "e"; }
1427 | else if (re3.test(w)) { re = re_1b_2; w = w.replace(re,""); }
1428 | else if (re4.test(w)) { w = w + "e"; }
1429 | }
1430 | }
1431 |
1432 | // Step 1c - replace suffix y or Y by i if preceded by a non-vowel which is not the first letter of the word (so cry -> cri, by -> by, say -> say)
1433 | re = re_1c;
1434 | if (re.test(w)) {
1435 | var fp = re.exec(w);
1436 | stem = fp[1];
1437 | w = stem + "i";
1438 | }
1439 |
1440 | // Step 2
1441 | re = re_2;
1442 | if (re.test(w)) {
1443 | var fp = re.exec(w);
1444 | stem = fp[1];
1445 | suffix = fp[2];
1446 | re = re_mgr0;
1447 | if (re.test(stem)) {
1448 | w = stem + step2list[suffix];
1449 | }
1450 | }
1451 |
1452 | // Step 3
1453 | re = re_3;
1454 | if (re.test(w)) {
1455 | var fp = re.exec(w);
1456 | stem = fp[1];
1457 | suffix = fp[2];
1458 | re = re_mgr0;
1459 | if (re.test(stem)) {
1460 | w = stem + step3list[suffix];
1461 | }
1462 | }
1463 |
1464 | // Step 4
1465 | re = re_4;
1466 | re2 = re2_4;
1467 | if (re.test(w)) {
1468 | var fp = re.exec(w);
1469 | stem = fp[1];
1470 | re = re_mgr1;
1471 | if (re.test(stem)) {
1472 | w = stem;
1473 | }
1474 | } else if (re2.test(w)) {
1475 | var fp = re2.exec(w);
1476 | stem = fp[1] + fp[2];
1477 | re2 = re_mgr1;
1478 | if (re2.test(stem)) {
1479 | w = stem;
1480 | }
1481 | }
1482 |
1483 | // Step 5
1484 | re = re_5;
1485 | if (re.test(w)) {
1486 | var fp = re.exec(w);
1487 | stem = fp[1];
1488 | re = re_mgr1;
1489 | re2 = re_meq1;
1490 | re3 = re3_5;
1491 | if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) {
1492 | w = stem;
1493 | }
1494 | }
1495 |
1496 | re = re_5_1;
1497 | re2 = re_mgr1;
1498 | if (re.test(w) && re2.test(w)) {
1499 | re = re_1b_2;
1500 | w = w.replace(re,"");
1501 | }
1502 |
1503 | // and turn initial Y back to y
1504 |
1505 | if (firstch == "y") {
1506 | w = firstch.toLowerCase() + w.substr(1);
1507 | }
1508 |
1509 | return w;
1510 | };
1511 |
1512 | return porterStemmer;
1513 | })();
1514 |
1515 | lunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer')
1516 | /*!
1517 | * lunr.stopWordFilter
1518 | * Copyright (C) 2014 Oliver Nightingale
1519 | */
1520 |
1521 | /**
1522 | * lunr.stopWordFilter is an English language stop word list filter, any words
1523 | * contained in the list will not be passed through the filter.
1524 | *
1525 | * This is intended to be used in the Pipeline. If the token does not pass the
1526 | * filter then undefined will be returned.
1527 | *
1528 | * @module
1529 | * @param {String} token The token to pass through the filter
1530 | * @returns {String}
1531 | * @see lunr.Pipeline
1532 | */
1533 | lunr.stopWordFilter = function (token) {
1534 | if (lunr.stopWordFilter.stopWords.indexOf(token) === -1) return token
1535 | }
1536 |
1537 | lunr.stopWordFilter.stopWords = new lunr.SortedSet
1538 | lunr.stopWordFilter.stopWords.length = 119
1539 | lunr.stopWordFilter.stopWords.elements = [
1540 | "",
1541 | "a",
1542 | "able",
1543 | "about",
1544 | "across",
1545 | "after",
1546 | "all",
1547 | "almost",
1548 | "also",
1549 | "am",
1550 | "among",
1551 | "an",
1552 | "and",
1553 | "any",
1554 | "are",
1555 | "as",
1556 | "at",
1557 | "be",
1558 | "because",
1559 | "been",
1560 | "but",
1561 | "by",
1562 | "can",
1563 | "cannot",
1564 | "could",
1565 | "dear",
1566 | "did",
1567 | "do",
1568 | "does",
1569 | "either",
1570 | "else",
1571 | "ever",
1572 | "every",
1573 | "for",
1574 | "from",
1575 | "get",
1576 | "got",
1577 | "had",
1578 | "has",
1579 | "have",
1580 | "he",
1581 | "her",
1582 | "hers",
1583 | "him",
1584 | "his",
1585 | "how",
1586 | "however",
1587 | "i",
1588 | "if",
1589 | "in",
1590 | "into",
1591 | "is",
1592 | "it",
1593 | "its",
1594 | "just",
1595 | "least",
1596 | "let",
1597 | "like",
1598 | "likely",
1599 | "may",
1600 | "me",
1601 | "might",
1602 | "most",
1603 | "must",
1604 | "my",
1605 | "neither",
1606 | "no",
1607 | "nor",
1608 | "not",
1609 | "of",
1610 | "off",
1611 | "often",
1612 | "on",
1613 | "only",
1614 | "or",
1615 | "other",
1616 | "our",
1617 | "own",
1618 | "rather",
1619 | "said",
1620 | "say",
1621 | "says",
1622 | "she",
1623 | "should",
1624 | "since",
1625 | "so",
1626 | "some",
1627 | "than",
1628 | "that",
1629 | "the",
1630 | "their",
1631 | "them",
1632 | "then",
1633 | "there",
1634 | "these",
1635 | "they",
1636 | "this",
1637 | "tis",
1638 | "to",
1639 | "too",
1640 | "twas",
1641 | "us",
1642 | "wants",
1643 | "was",
1644 | "we",
1645 | "were",
1646 | "what",
1647 | "when",
1648 | "where",
1649 | "which",
1650 | "while",
1651 | "who",
1652 | "whom",
1653 | "why",
1654 | "will",
1655 | "with",
1656 | "would",
1657 | "yet",
1658 | "you",
1659 | "your"
1660 | ]
1661 |
1662 | lunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter')
1663 | /*!
1664 | * lunr.trimmer
1665 | * Copyright (C) 2014 Oliver Nightingale
1666 | */
1667 |
1668 | /**
1669 | * lunr.trimmer is a pipeline function for trimming non word
1670 | * characters from the begining and end of tokens before they
1671 | * enter the index.
1672 | *
1673 | * This implementation may not work correctly for non latin
1674 | * characters and should either be removed or adapted for use
1675 | * with languages with non-latin characters.
1676 | *
1677 | * @module
1678 | * @param {String} token The token to pass through the filter
1679 | * @returns {String}
1680 | * @see lunr.Pipeline
1681 | */
1682 | lunr.trimmer = function (token) {
1683 | return token
1684 | .replace(/^\W+/, '')
1685 | .replace(/\W+$/, '')
1686 | }
1687 |
1688 | lunr.Pipeline.registerFunction(lunr.trimmer, 'trimmer')
1689 | /*!
1690 | * lunr.stemmer
1691 | * Copyright (C) 2014 Oliver Nightingale
1692 | * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt
1693 | */
1694 |
1695 | /**
1696 | * lunr.TokenStore is used for efficient storing and lookup of the reverse
1697 | * index of token to document ref.
1698 | *
1699 | * @constructor
1700 | */
1701 | lunr.TokenStore = function () {
1702 | this.root = { docs: {} }
1703 | this.length = 0
1704 | }
1705 |
1706 | /**
1707 | * Loads a previously serialised token store
1708 | *
1709 | * @param {Object} serialisedData The serialised token store to load.
1710 | * @returns {lunr.TokenStore}
1711 | * @memberOf TokenStore
1712 | */
1713 | lunr.TokenStore.load = function (serialisedData) {
1714 | var store = new this
1715 |
1716 | store.root = serialisedData.root
1717 | store.length = serialisedData.length
1718 |
1719 | return store
1720 | }
1721 |
1722 | /**
1723 | * Adds a new token doc pair to the store.
1724 | *
1725 | * By default this function starts at the root of the current store, however
1726 | * it can start at any node of any token store if required.
1727 | *
1728 | * @param {String} token The token to store the doc under
1729 | * @param {Object} doc The doc to store against the token
1730 | * @param {Object} root An optional node at which to start looking for the
1731 | * correct place to enter the doc, by default the root of this lunr.TokenStore
1732 | * is used.
1733 | * @memberOf TokenStore
1734 | */
1735 | lunr.TokenStore.prototype.add = function (token, doc, root) {
1736 | var root = root || this.root,
1737 | key = token[0],
1738 | rest = token.slice(1)
1739 |
1740 | if (!(key in root)) root[key] = {docs: {}}
1741 |
1742 | if (rest.length === 0) {
1743 | root[key].docs[doc.ref] = doc
1744 | this.length += 1
1745 | return
1746 | } else {
1747 | return this.add(rest, doc, root[key])
1748 | }
1749 | }
1750 |
1751 | /**
1752 | * Checks whether this key is contained within this lunr.TokenStore.
1753 | *
1754 | * By default this function starts at the root of the current store, however
1755 | * it can start at any node of any token store if required.
1756 | *
1757 | * @param {String} token The token to check for
1758 | * @param {Object} root An optional node at which to start
1759 | * @memberOf TokenStore
1760 | */
1761 | lunr.TokenStore.prototype.has = function (token) {
1762 | if (!token) return false
1763 |
1764 | var node = this.root
1765 |
1766 | for (var i = 0; i < token.length; i++) {
1767 | if (!node[token[i]]) return false
1768 |
1769 | node = node[token[i]]
1770 | }
1771 |
1772 | return true
1773 | }
1774 |
1775 | /**
1776 | * Retrieve a node from the token store for a given token.
1777 | *
1778 | * By default this function starts at the root of the current store, however
1779 | * it can start at any node of any token store if required.
1780 | *
1781 | * @param {String} token The token to get the node for.
1782 | * @param {Object} root An optional node at which to start.
1783 | * @returns {Object}
1784 | * @see TokenStore.prototype.get
1785 | * @memberOf TokenStore
1786 | */
1787 | lunr.TokenStore.prototype.getNode = function (token) {
1788 | if (!token) return {}
1789 |
1790 | var node = this.root
1791 |
1792 | for (var i = 0; i < token.length; i++) {
1793 | if (!node[token[i]]) return {}
1794 |
1795 | node = node[token[i]]
1796 | }
1797 |
1798 | return node
1799 | }
1800 |
1801 | /**
1802 | * Retrieve the documents for a node for the given token.
1803 | *
1804 | * By default this function starts at the root of the current store, however
1805 | * it can start at any node of any token store if required.
1806 | *
1807 | * @param {String} token The token to get the documents for.
1808 | * @param {Object} root An optional node at which to start.
1809 | * @returns {Object}
1810 | * @memberOf TokenStore
1811 | */
1812 | lunr.TokenStore.prototype.get = function (token, root) {
1813 | return this.getNode(token, root).docs || {}
1814 | }
1815 |
1816 | lunr.TokenStore.prototype.count = function (token, root) {
1817 | return Object.keys(this.get(token, root)).length
1818 | }
1819 |
1820 | /**
1821 | * Remove the document identified by ref from the token in the store.
1822 | *
1823 | * By default this function starts at the root of the current store, however
1824 | * it can start at any node of any token store if required.
1825 | *
1826 | * @param {String} token The token to get the documents for.
1827 | * @param {String} ref The ref of the document to remove from this token.
1828 | * @param {Object} root An optional node at which to start.
1829 | * @returns {Object}
1830 | * @memberOf TokenStore
1831 | */
1832 | lunr.TokenStore.prototype.remove = function (token, ref) {
1833 | if (!token) return
1834 | var node = this.root
1835 |
1836 | for (var i = 0; i < token.length; i++) {
1837 | if (!(token[i] in node)) return
1838 | node = node[token[i]]
1839 | }
1840 |
1841 | delete node.docs[ref]
1842 | }
1843 |
1844 | /**
1845 | * Find all the possible suffixes of the passed token using tokens
1846 | * currently in the store.
1847 | *
1848 | * @param {String} token The token to expand.
1849 | * @returns {Array}
1850 | * @memberOf TokenStore
1851 | */
1852 | lunr.TokenStore.prototype.expand = function (token, memo) {
1853 | var root = this.getNode(token),
1854 | docs = root.docs || {},
1855 | memo = memo || []
1856 |
1857 | if (Object.keys(docs).length) memo.push(token)
1858 |
1859 | Object.keys(root)
1860 | .forEach(function (key) {
1861 | if (key === 'docs') return
1862 |
1863 | memo.concat(this.expand(token + key, memo))
1864 | }, this)
1865 |
1866 | return memo
1867 | }
1868 |
1869 | /**
1870 | * Returns a representation of the token store ready for serialisation.
1871 | *
1872 | * @returns {Object}
1873 | * @memberOf TokenStore
1874 | */
1875 | lunr.TokenStore.prototype.toJSON = function () {
1876 | return {
1877 | root: this.root,
1878 | length: this.length
1879 | }
1880 | }
1881 |
1882 |
1883 | /**
1884 | * export the module via AMD, CommonJS or as a browser global
1885 | * Export code from https://github.com/umdjs/umd/blob/master/returnExports.js
1886 | */
1887 | ;(function (root, factory) {
1888 | if (typeof define === 'function' && define.amd) {
1889 | // AMD. Register as an anonymous module.
1890 | define(factory)
1891 | } else if (typeof exports === 'object') {
1892 | /**
1893 | * Node. Does not work with strict CommonJS, but
1894 | * only CommonJS-like enviroments that support module.exports,
1895 | * like Node.
1896 | */
1897 | module.exports = factory()
1898 | } else {
1899 | // Browser globals (root is window)
1900 | root.lunr = factory()
1901 | }
1902 | }(this, function () {
1903 | /**
1904 | * Just return a value to define the module export.
1905 | * This example returns an object, but the module
1906 | * can return a function as the exported value.
1907 | */
1908 | return lunr
1909 | }))
1910 | })()
1911 |
--------------------------------------------------------------------------------