diff --git a/source/fonts/icomoon.eot b/source/fonts/icomoon.eot old mode 100644 new mode 100755 index 109f6c1..212835a Binary files a/source/fonts/icomoon.eot and b/source/fonts/icomoon.eot differ diff --git a/source/fonts/icomoon.svg b/source/fonts/icomoon.svg old mode 100644 new mode 100755 index e23b01c..2449358 --- a/source/fonts/icomoon.svg +++ b/source/fonts/icomoon.svg @@ -3,15 +3,16 @@ Generated by IcoMoon - - - - - - - - - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/source/fonts/icomoon.ttf b/source/fonts/icomoon.ttf old mode 100644 new mode 100755 index 455bd6d..360b3d7 Binary files a/source/fonts/icomoon.ttf and b/source/fonts/icomoon.ttf differ diff --git a/source/fonts/icomoon.woff b/source/fonts/icomoon.woff old mode 100644 new mode 100755 index 3b3ee94..9a213b1 Binary files a/source/fonts/icomoon.woff and b/source/fonts/icomoon.woff differ diff --git a/source/javascripts/all.js b/source/javascripts/all.js index 9eae26e..534eae9 100644 --- a/source/javascripts/all.js +++ b/source/javascripts/all.js @@ -1,2 +1,2 @@ -//= require './jquery_ui' -//= require_tree . \ No newline at end of file +//= require_tree ./lib +//= require_tree ./app diff --git a/source/javascripts/app/lang.js b/source/javascripts/app/lang.js new file mode 100644 index 0000000..1d0685f --- /dev/null +++ b/source/javascripts/app/lang.js @@ -0,0 +1,58 @@ +/* +Copyright 2008-2013 Concur Technologies, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain +a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. +*/ +(function (global) { + var languages = []; + + global.setupLanguages = setupLanguages; + global.activateLanguage = activateLanguage; + + function activateLanguage(language) { + $("#lang-selector a").removeClass('active'); + $("#lang-selector a[data-language-name='" + language + "']").addClass('active'); + for (var i=0; i < languages.length; i++) { + $(".highlight." + languages[i]).hide(); + } + $(".highlight." + language).show(); + } + + function setupLanguages(l) { + var currentLanguage = l[0]; + var defaultLanguage = localStorage.getItem("language"); + + languages = l; + + if ((location.search.substr(1) != "") && (jQuery.inArray(location.search.substr(1), languages)) != -1) { + // the language is in the URL, so use that language! + activateLanguage(location.search.substr(1)); + + // set this language as the default for next time, if the URL has no language + localStorage.setItem("language", location.search.substr(1)); + } else if ((defaultLanguage !== null) && (jQuery.inArray(defaultLanguage, languages) != -1)) { + // the language was the last selected one saved in localstorage, so use that language! + activateLanguage(defaultLanguage); + } else { + // no language selected, so use the default + activateLanguage(languages[0]); + } + + // if we click on a language tab, reload the page with that language in the URL + $("#lang-selector a").bind("click", function() { + window.location.replace("?" + $(this).data("language-name") + window.location.hash); + return false; + }); + + } +})(window); diff --git a/source/javascripts/app/search.js b/source/javascripts/app/search.js new file mode 100644 index 0000000..d443a39 --- /dev/null +++ b/source/javascripts/app/search.js @@ -0,0 +1,100 @@ +(function (global) { + + var $global = $(global); + var content, darkBox, searchInfo; + var highlightOpts = { element: 'span', className: 'search-highlight' }; + + var index = new lunr.Index; + + index.ref('id'); + index.field('title', { boost: 10 }); + index.field('body'); + index.pipeline.add(lunr.trimmer, lunr.stopWordFilter); + + $(populate); + $(bind); + + function populate () { + $('h1').each(function () { + var title = $(this); + var body = title.nextUntil('h1'); + var wrapper = $('
'); + + title.after(wrapper.append(body)); + wrapper.prepend(title); + + index.add({ + id: title.prop('id'), + title: title.text(), + body: body.text() + }); + }); + } + + function bind () { + content = $('.content'); + darkBox = $('.dark-box'); + searchInfo = $('.search-info'); + + $('#input-search') + .on('keyup', search) + .on('focus', active) + .on('blur', inactive); + + $global.on('resize', resize); + resize(); + } + + function search (event) { + var sections = $('section, #toc .tocify-header'); + + searchInfo.hide(); + unhighlight(); + + // ESC clears the field + if (event.keyCode === 27) this.value = ''; + + if (this.value) { + sections.hide(); + var results = index.search(this.value); + if (results.length) { + $.each(results, function (index, item) { + $('#section-' + item.ref).show(); + $('.tocify-item[data-unique=' + item.ref + ']').closest('.tocify-header').show(); + }); + highlight.call(this); + } else { + sections.show(); + searchInfo.text('No Results Found for "' + this.value + '"').show(); + } + } else { + sections.show(); + } + + // HACK trigger tocify height recalculation + $global.triggerHandler('scroll.tocify'); + $global.triggerHandler('resize'); + } + + function active () { + search.call(this, {}); + } + + function inactive () { + unhighlight(); + searchInfo.hide(); + } + + function highlight () { + if (this.value) content.highlight(this.value, highlightOpts); + } + + function unhighlight () { + content.unhighlight(highlightOpts); + } + + function resize () { + searchInfo.innerWidth(content.innerWidth() - darkBox.innerWidth()); + } + +})(window); diff --git a/source/javascripts/app/toc.js b/source/javascripts/app/toc.js new file mode 100644 index 0000000..3761c2b --- /dev/null +++ b/source/javascripts/app/toc.js @@ -0,0 +1,37 @@ +(function (global) { + + var toc; + + global.toc = toc; + + $(toc); + $(animate); + + function toc () { + toc = $("#toc").tocify({ + selectors: 'h1, h2', + extendPage: false, + theme: 'none', + smoothScroll: false, + showEffectSpeed: 0, + hideEffectSpeed: 180, + ignoreSelector: '.toc-ignore', + highlightOffset: 60, + scrollTo: -2, + scrollHistory: true, + hashGenerator: function (text, element) { + return element.prop('id'); + } + }).data('toc-tocify'); + } + + // Hack to make already open sections to start opened, + // instead of displaying an ugly animation + function animate () { + setTimeout(function() { + toc.setOption('showEffectSpeed', 180); + }, 50); + } + +})(window); + diff --git a/source/javascripts/lang_selector.js b/source/javascripts/lang_selector.js deleted file mode 100644 index 4e3dbc9..0000000 --- a/source/javascripts/lang_selector.js +++ /dev/null @@ -1,52 +0,0 @@ -/* -Copyright 2008-2013 Concur Technologies, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); you may -not use this file except in compliance with the License. You may obtain -a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -License for the specific language governing permissions and limitations -under the License. -*/ -languages = [] -function activateLanguage(language) { - $("#lang-selector a").removeClass('active'); - $("#lang-selector a[data-language-name='" + language + "']").addClass('active'); - for (var i=0; i < languages.length; i++) { - $(".highlight." + languages[i]).hide(); - } - $(".highlight." + language).show(); - -} - -function setupLanguages(l) { - languages = l; - currentLanguage = languages[0]; - defaultLanguage = localStorage.getItem("language"); - - if ((location.search.substr(1) != "") && (jQuery.inArray(location.search.substr(1), languages)) != -1) { - // the language is in the URL, so use that language! - activateLanguage(location.search.substr(1)); - - // set this language as the default for next time, if the URL has no language - localStorage.setItem("language", location.search.substr(1)); - } else if ((defaultLanguage !== null) && (jQuery.inArray(defaultLanguage, languages) != -1)) { - // the language was the last selected one saved in localstorage, so use that language! - activateLanguage(defaultLanguage); - } else { - // no language selected, so use the default - activateLanguage(languages[0]); - } - - // if we click on a language tab, reload the page with that language in the URL - $("#lang-selector a").bind("click", function() { - window.location.replace("?" + $(this).data("language-name") + window.location.hash); - return false; - }); - -} diff --git a/source/javascripts/lib/jquery.highlight.js b/source/javascripts/lib/jquery.highlight.js new file mode 100644 index 0000000..9dcf3c7 --- /dev/null +++ b/source/javascripts/lib/jquery.highlight.js @@ -0,0 +1,108 @@ +/* + * jQuery Highlight plugin + * + * Based on highlight v3 by Johann Burkard + * http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html + * + * Code a little bit refactored and cleaned (in my humble opinion). + * Most important changes: + * - has an option to highlight only entire words (wordsOnly - false by default), + * - has an option to be case sensitive (caseSensitive - false by default) + * - highlight element tag and class names can be specified in options + * + * Usage: + * // wrap every occurrance of text 'lorem' in content + * // with (default options) + * $('#content').highlight('lorem'); + * + * // search for and highlight more terms at once + * // so you can save some time on traversing DOM + * $('#content').highlight(['lorem', 'ipsum']); + * $('#content').highlight('lorem ipsum'); + * + * // search only for entire word 'lorem' + * $('#content').highlight('lorem', { wordsOnly: true }); + * + * // don't ignore case during search of term 'lorem' + * $('#content').highlight('lorem', { caseSensitive: true }); + * + * // wrap every occurrance of term 'ipsum' in content + * // with + * $('#content').highlight('ipsum', { element: 'em', className: 'important' }); + * + * // remove default highlight + * $('#content').unhighlight(); + * + * // remove custom highlight + * $('#content').unhighlight({ element: 'em', className: 'important' }); + * + * + * Copyright (c) 2009 Bartek Szopka + * + * Licensed under MIT license. + * + */ + +jQuery.extend({ + highlight: function (node, re, nodeName, className) { + if (node.nodeType === 3) { + var match = node.data.match(re); + if (match) { + var highlight = document.createElement(nodeName || 'span'); + highlight.className = className || 'highlight'; + var wordNode = node.splitText(match.index); + wordNode.splitText(match[0].length); + var wordClone = wordNode.cloneNode(true); + highlight.appendChild(wordClone); + wordNode.parentNode.replaceChild(highlight, wordNode); + return 1; //skip added node in parent + } + } else if ((node.nodeType === 1 && node.childNodes) && // only element nodes that have children + !/(script|style)/i.test(node.tagName) && // ignore script and style nodes + !(node.tagName === nodeName.toUpperCase() && node.className === className)) { // skip if already highlighted + for (var i = 0; i < node.childNodes.length; i++) { + i += jQuery.highlight(node.childNodes[i], re, nodeName, className); + } + } + return 0; + } +}); + +jQuery.fn.unhighlight = function (options) { + var settings = { className: 'highlight', element: 'span' }; + jQuery.extend(settings, options); + + return this.find(settings.element + "." + settings.className).each(function () { + var parent = this.parentNode; + parent.replaceChild(this.firstChild, this); + parent.normalize(); + }).end(); +}; + +jQuery.fn.highlight = function (words, options) { + var settings = { className: 'highlight', element: 'span', caseSensitive: false, wordsOnly: false }; + jQuery.extend(settings, options); + + if (words.constructor === String) { + words = [words]; + } + words = jQuery.grep(words, function(word, i){ + return word != ''; + }); + words = jQuery.map(words, function(word, i) { + return word.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + }); + if (words.length == 0) { return this; }; + + var flag = settings.caseSensitive ? "" : "i"; + var pattern = "(" + words.join("|") + ")"; + if (settings.wordsOnly) { + pattern = "\\b" + pattern + "\\b"; + } + var re = new RegExp(pattern, flag); + + return this.each(function () { + jQuery.highlight(this, re, settings.element, settings.className); + }); +}; + diff --git a/source/javascripts/jquery.tocify.js b/source/javascripts/lib/jquery.tocify.js similarity index 99% rename from source/javascripts/jquery.tocify.js rename to source/javascripts/lib/jquery.tocify.js index 6f4c927..a48d8be 100644 --- a/source/javascripts/jquery.tocify.js +++ b/source/javascripts/lib/jquery.tocify.js @@ -1,3 +1,4 @@ +//= require ./jquery_ui /* jquery Tocify - v1.8.0 - 2013-09-16 * http://www.gregfranko.com/jquery.tocify.js/ * Copyright (c) 2013 Greg Franko; Licensed MIT @@ -1022,4 +1023,4 @@ }); -})); //end of plugin \ No newline at end of file +})); //end of plugin diff --git a/source/javascripts/jquery_ui.js b/source/javascripts/lib/jquery_ui.js similarity index 100% rename from source/javascripts/jquery_ui.js rename to source/javascripts/lib/jquery_ui.js diff --git a/source/javascripts/lib/lunr.js b/source/javascripts/lib/lunr.js new file mode 100644 index 0000000..bd221b7 --- /dev/null +++ b/source/javascripts/lib/lunr.js @@ -0,0 +1,1880 @@ +/** + * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 0.5.2 + * Copyright (C) 2014 Oliver Nightingale + * MIT Licensed + * @license + */ + +(function(){ + +/** + * Convenience function for instantiating a new lunr index and configuring it + * with the default pipeline functions and the passed config function. + * + * When using this convenience function a new index will be created with the + * following functions already in the pipeline: + * + * lunr.StopWordFilter - filters out any stop words before they enter the + * index + * + * lunr.stemmer - stems the tokens before entering the index. + * + * Example: + * + * var idx = lunr(function () { + * this.field('title', 10) + * this.field('tags', 100) + * this.field('body') + * + * this.ref('cid') + * + * this.pipeline.add(function () { + * // some custom pipeline function + * }) + * + * }) + * + * @param {Function} config A function that will be called with the new instance + * of the lunr.Index as both its context and first parameter. It can be used to + * customize the instance of new lunr.Index. + * @namespace + * @module + * @returns {lunr.Index} + * + */ +var lunr = function (config) { + var idx = new lunr.Index + + idx.pipeline.add( + lunr.trimmer, + lunr.stopWordFilter, + lunr.stemmer + ) + + if (config) config.call(idx, idx) + + return idx +} + +lunr.version = "0.5.2" +/*! + * lunr.utils + * Copyright (C) 2014 Oliver Nightingale + */ + +/** + * A namespace containing utils for the rest of the lunr library + */ +lunr.utils = {} + +/** + * Print a warning message to the console. + * + * @param {String} message The message to be printed. + * @memberOf Utils + */ +lunr.utils.warn = (function (global) { + return function (message) { + if (global.console && console.warn) { + console.warn(message) + } + } +})(this) + +/*! + * lunr.EventEmitter + * Copyright (C) 2014 Oliver Nightingale + */ + +/** + * lunr.EventEmitter is an event emitter for lunr. It manages adding and removing event handlers and triggering events and their handlers. + * + * @constructor + */ +lunr.EventEmitter = function () { + this.events = {} +} + +/** + * Binds a handler function to a specific event(s). + * + * Can bind a single function to many different events in one call. + * + * @param {String} [eventName] The name(s) of events to bind this function to. + * @param {Function} handler The function to call when an event is fired. + * @memberOf EventEmitter + */ +lunr.EventEmitter.prototype.addListener = function () { + var args = Array.prototype.slice.call(arguments), + fn = args.pop(), + names = args + + if (typeof fn !== "function") throw new TypeError ("last argument must be a function") + + names.forEach(function (name) { + if (!this.hasHandler(name)) this.events[name] = [] + this.events[name].push(fn) + }, this) +} + +/** + * Removes a handler function from a specific event. + * + * @param {String} eventName The name of the event to remove this function from. + * @param {Function} handler The function to remove from an event. + * @memberOf EventEmitter + */ +lunr.EventEmitter.prototype.removeListener = function (name, fn) { + if (!this.hasHandler(name)) return + + var fnIndex = this.events[name].indexOf(fn) + this.events[name].splice(fnIndex, 1) + + if (!this.events[name].length) delete this.events[name] +} + +/** + * Calls all functions bound to the given event. + * + * Additional data can be passed to the event handler as arguments to `emit` + * after the event name. + * + * @param {String} eventName The name of the event to emit. + * @memberOf EventEmitter + */ +lunr.EventEmitter.prototype.emit = function (name) { + if (!this.hasHandler(name)) return + + var args = Array.prototype.slice.call(arguments, 1) + + this.events[name].forEach(function (fn) { + fn.apply(undefined, args) + }) +} + +/** + * Checks whether a handler has ever been stored against an event. + * + * @param {String} eventName The name of the event to check. + * @private + * @memberOf EventEmitter + */ +lunr.EventEmitter.prototype.hasHandler = function (name) { + return name in this.events +} + +/*! + * lunr.tokenizer + * Copyright (C) 2014 Oliver Nightingale + */ + +/** + * A function for splitting a string into tokens ready to be inserted into + * the search index. + * + * @module + * @param {String} obj The string to convert into tokens + * @returns {Array} + */ +lunr.tokenizer = function (obj) { + if (!arguments.length || obj == null || obj == undefined) return [] + if (Array.isArray(obj)) return obj.map(function (t) { return t.toLowerCase() }) + + var str = obj.toString().replace(/^\s+/, '') + + for (var i = str.length - 1; i >= 0; i--) { + if (/\S/.test(str.charAt(i))) { + str = str.substring(0, i + 1) + break + } + } + + return str + .split(/\s+/) + .map(function (token) { + return token.toLowerCase() + }) +} +/*! + * lunr.Pipeline + * Copyright (C) 2014 Oliver Nightingale + */ + +/** + * lunr.Pipelines maintain an ordered list of functions to be applied to all + * tokens in documents entering the search index and queries being ran against + * the index. + * + * An instance of lunr.Index created with the lunr shortcut will contain a + * pipeline with a stop word filter and an English language stemmer. Extra + * functions can be added before or after either of these functions or these + * default functions can be removed. + * + * When run the pipeline will call each function in turn, passing a token, the + * index of that token in the original list of all tokens and finally a list of + * all the original tokens. + * + * The output of functions in the pipeline will be passed to the next function + * in the pipeline. To exclude a token from entering the index the function + * should return undefined, the rest of the pipeline will not be called with + * this token. + * + * For serialisation of pipelines to work, all functions used in an instance of + * a pipeline should be registered with lunr.Pipeline. Registered functions can + * then be loaded. If trying to load a serialised pipeline that uses functions + * that are not registered an error will be thrown. + * + * If not planning on serialising the pipeline then registering pipeline functions + * is not necessary. + * + * @constructor + */ +lunr.Pipeline = function () { + this._stack = [] +} + +lunr.Pipeline.registeredFunctions = {} + +/** + * Register a function with the pipeline. + * + * Functions that are used in the pipeline should be registered if the pipeline + * needs to be serialised, or a serialised pipeline needs to be loaded. + * + * Registering a function does not add it to a pipeline, functions must still be + * added to instances of the pipeline for them to be used when running a pipeline. + * + * @param {Function} fn The function to check for. + * @param {String} label The label to register this function with + * @memberOf Pipeline + */ +lunr.Pipeline.registerFunction = function (fn, label) { + if (label in this.registeredFunctions) { + lunr.utils.warn('Overwriting existing registered function: ' + label) + } + + fn.label = label + lunr.Pipeline.registeredFunctions[fn.label] = fn +} + +/** + * Warns if the function is not registered as a Pipeline function. + * + * @param {Function} fn The function to check for. + * @private + * @memberOf Pipeline + */ +lunr.Pipeline.warnIfFunctionNotRegistered = function (fn) { + var isRegistered = fn.label && (fn.label in this.registeredFunctions) + + if (!isRegistered) { + lunr.utils.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\n', fn) + } +} + +/** + * Loads a previously serialised pipeline. + * + * All functions to be loaded must already be registered with lunr.Pipeline. + * If any function from the serialised data has not been registered then an + * error will be thrown. + * + * @param {Object} serialised The serialised pipeline to load. + * @returns {lunr.Pipeline} + * @memberOf Pipeline + */ +lunr.Pipeline.load = function (serialised) { + var pipeline = new lunr.Pipeline + + serialised.forEach(function (fnName) { + var fn = lunr.Pipeline.registeredFunctions[fnName] + + if (fn) { + pipeline.add(fn) + } else { + throw new Error ('Cannot load un-registered function: ' + fnName) + } + }) + + return pipeline +} + +/** + * Adds new functions to the end of the pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {Function} functions Any number of functions to add to the pipeline. + * @memberOf Pipeline + */ +lunr.Pipeline.prototype.add = function () { + var fns = Array.prototype.slice.call(arguments) + + fns.forEach(function (fn) { + lunr.Pipeline.warnIfFunctionNotRegistered(fn) + this._stack.push(fn) + }, this) +} + +/** + * Adds a single function after a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {Function} existingFn A function that already exists in the pipeline. + * @param {Function} newFn The new function to add to the pipeline. + * @memberOf Pipeline + */ +lunr.Pipeline.prototype.after = function (existingFn, newFn) { + lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + + var pos = this._stack.indexOf(existingFn) + 1 + this._stack.splice(pos, 0, newFn) +} + +/** + * Adds a single function before a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {Function} existingFn A function that already exists in the pipeline. + * @param {Function} newFn The new function to add to the pipeline. + * @memberOf Pipeline + */ +lunr.Pipeline.prototype.before = function (existingFn, newFn) { + lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + + var pos = this._stack.indexOf(existingFn) + this._stack.splice(pos, 0, newFn) +} + +/** + * Removes a function from the pipeline. + * + * @param {Function} fn The function to remove from the pipeline. + * @memberOf Pipeline + */ +lunr.Pipeline.prototype.remove = function (fn) { + var pos = this._stack.indexOf(fn) + this._stack.splice(pos, 1) +} + +/** + * Runs the current list of functions that make up the pipeline against the + * passed tokens. + * + * @param {Array} tokens The tokens to run through the pipeline. + * @returns {Array} + * @memberOf Pipeline + */ +lunr.Pipeline.prototype.run = function (tokens) { + var out = [], + tokenLength = tokens.length, + stackLength = this._stack.length + + for (var i = 0; i < tokenLength; i++) { + var token = tokens[i] + + for (var j = 0; j < stackLength; j++) { + token = this._stack[j](token, i, tokens) + if (token === void 0) break + }; + + if (token !== void 0) out.push(token) + }; + + return out +} + +/** + * Resets the pipeline by removing any existing processors. + * + * @memberOf Pipeline + */ +lunr.Pipeline.prototype.reset = function () { + this._stack = [] +} + +/** + * Returns a representation of the pipeline ready for serialisation. + * + * Logs a warning if the function has not been registered. + * + * @returns {Array} + * @memberOf Pipeline + */ +lunr.Pipeline.prototype.toJSON = function () { + return this._stack.map(function (fn) { + lunr.Pipeline.warnIfFunctionNotRegistered(fn) + + return fn.label + }) +} +/*! + * lunr.Vector + * Copyright (C) 2014 Oliver Nightingale + */ + +/** + * lunr.Vectors implement vector related operations for + * a series of elements. + * + * @constructor + */ +lunr.Vector = function () { + this._magnitude = null + this.list = undefined + this.length = 0 +} + +/** + * lunr.Vector.Node is a simple struct for each node + * in a lunr.Vector. + * + * @private + * @param {Number} The index of the node in the vector. + * @param {Object} The data at this node in the vector. + * @param {lunr.Vector.Node} The node directly after this node in the vector. + * @constructor + * @memberOf Vector + */ +lunr.Vector.Node = function (idx, val, next) { + this.idx = idx + this.val = val + this.next = next +} + +/** + * Inserts a new value at a position in a vector. + * + * @param {Number} The index at which to insert a value. + * @param {Object} The object to insert in the vector. + * @memberOf Vector. + */ +lunr.Vector.prototype.insert = function (idx, val) { + var list = this.list + + if (!list) { + this.list = new lunr.Vector.Node (idx, val, list) + return this.length++ + } + + var prev = list, + next = list.next + + while (next != undefined) { + if (idx < next.idx) { + prev.next = new lunr.Vector.Node (idx, val, next) + return this.length++ + } + + prev = next, next = next.next + } + + prev.next = new lunr.Vector.Node (idx, val, next) + return this.length++ +} + +/** + * Calculates the magnitude of this vector. + * + * @returns {Number} + * @memberOf Vector + */ +lunr.Vector.prototype.magnitude = function () { + if (this._magniture) return this._magnitude + var node = this.list, + sumOfSquares = 0, + val + + while (node) { + val = node.val + sumOfSquares += val * val + node = node.next + } + + return this._magnitude = Math.sqrt(sumOfSquares) +} + +/** + * Calculates the dot product of this vector and another vector. + * + * @param {lunr.Vector} otherVector The vector to compute the dot product with. + * @returns {Number} + * @memberOf Vector + */ +lunr.Vector.prototype.dot = function (otherVector) { + var node = this.list, + otherNode = otherVector.list, + dotProduct = 0 + + while (node && otherNode) { + if (node.idx < otherNode.idx) { + node = node.next + } else if (node.idx > otherNode.idx) { + otherNode = otherNode.next + } else { + dotProduct += node.val * otherNode.val + node = node.next + otherNode = otherNode.next + } + } + + return dotProduct +} + +/** + * Calculates the cosine similarity between this vector and another + * vector. + * + * @param {lunr.Vector} otherVector The other vector to calculate the + * similarity with. + * @returns {Number} + * @memberOf Vector + */ +lunr.Vector.prototype.similarity = function (otherVector) { + return this.dot(otherVector) / (this.magnitude() * otherVector.magnitude()) +} +/*! + * lunr.SortedSet + * Copyright (C) 2014 Oliver Nightingale + */ + +/** + * lunr.SortedSets are used to maintain an array of uniq values in a sorted + * order. + * + * @constructor + */ +lunr.SortedSet = function () { + this.length = 0 + this.elements = [] +} + +/** + * Loads a previously serialised sorted set. + * + * @param {Array} serialisedData The serialised set to load. + * @returns {lunr.SortedSet} + * @memberOf SortedSet + */ +lunr.SortedSet.load = function (serialisedData) { + var set = new this + + set.elements = serialisedData + set.length = serialisedData.length + + return set +} + +/** + * Inserts new items into the set in the correct position to maintain the + * order. + * + * @param {Object} The objects to add to this set. + * @memberOf SortedSet + */ +lunr.SortedSet.prototype.add = function () { + Array.prototype.slice.call(arguments).forEach(function (element) { + if (~this.indexOf(element)) return + this.elements.splice(this.locationFor(element), 0, element) + }, this) + + this.length = this.elements.length +} + +/** + * Converts this sorted set into an array. + * + * @returns {Array} + * @memberOf SortedSet + */ +lunr.SortedSet.prototype.toArray = function () { + return this.elements.slice() +} + +/** + * Creates a new array with the results of calling a provided function on every + * element in this sorted set. + * + * Delegates to Array.prototype.map and has the same signature. + * + * @param {Function} fn The function that is called on each element of the + * set. + * @param {Object} ctx An optional object that can be used as the context + * for the function fn. + * @returns {Array} + * @memberOf SortedSet + */ +lunr.SortedSet.prototype.map = function (fn, ctx) { + return this.elements.map(fn, ctx) +} + +/** + * Executes a provided function once per sorted set element. + * + * Delegates to Array.prototype.forEach and has the same signature. + * + * @param {Function} fn The function that is called on each element of the + * set. + * @param {Object} ctx An optional object that can be used as the context + * @memberOf SortedSet + * for the function fn. + */ +lunr.SortedSet.prototype.forEach = function (fn, ctx) { + return this.elements.forEach(fn, ctx) +} + +/** + * Returns the index at which a given element can be found in the + * sorted set, or -1 if it is not present. + * + * @param {Object} elem The object to locate in the sorted set. + * @param {Number} start An optional index at which to start searching from + * within the set. + * @param {Number} end An optional index at which to stop search from within + * the set. + * @returns {Number} + * @memberOf SortedSet + */ +lunr.SortedSet.prototype.indexOf = function (elem, start, end) { + var start = start || 0, + end = end || this.elements.length, + sectionLength = end - start, + pivot = start + Math.floor(sectionLength / 2), + pivotElem = this.elements[pivot] + + if (sectionLength <= 1) { + if (pivotElem === elem) { + return pivot + } else { + return -1 + } + } + + if (pivotElem < elem) return this.indexOf(elem, pivot, end) + if (pivotElem > elem) return this.indexOf(elem, start, pivot) + if (pivotElem === elem) return pivot +} + +/** + * Returns the position within the sorted set that an element should be + * inserted at to maintain the current order of the set. + * + * This function assumes that the element to search for does not already exist + * in the sorted set. + * + * @param {Object} elem The elem to find the position for in the set + * @param {Number} start An optional index at which to start searching from + * within the set. + * @param {Number} end An optional index at which to stop search from within + * the set. + * @returns {Number} + * @memberOf SortedSet + */ +lunr.SortedSet.prototype.locationFor = function (elem, start, end) { + var start = start || 0, + end = end || this.elements.length, + sectionLength = end - start, + pivot = start + Math.floor(sectionLength / 2), + pivotElem = this.elements[pivot] + + if (sectionLength <= 1) { + if (pivotElem > elem) return pivot + if (pivotElem < elem) return pivot + 1 + } + + if (pivotElem < elem) return this.locationFor(elem, pivot, end) + if (pivotElem > elem) return this.locationFor(elem, start, pivot) +} + +/** + * Creates a new lunr.SortedSet that contains the elements in the intersection + * of this set and the passed set. + * + * @param {lunr.SortedSet} otherSet The set to intersect with this set. + * @returns {lunr.SortedSet} + * @memberOf SortedSet + */ +lunr.SortedSet.prototype.intersect = function (otherSet) { + var intersectSet = new lunr.SortedSet, + i = 0, j = 0, + a_len = this.length, b_len = otherSet.length, + a = this.elements, b = otherSet.elements + + while (true) { + if (i > a_len - 1 || j > b_len - 1) break + + if (a[i] === b[j]) { + intersectSet.add(a[i]) + i++, j++ + continue + } + + if (a[i] < b[j]) { + i++ + continue + } + + if (a[i] > b[j]) { + j++ + continue + } + }; + + return intersectSet +} + +/** + * Makes a copy of this set + * + * @returns {lunr.SortedSet} + * @memberOf SortedSet + */ +lunr.SortedSet.prototype.clone = function () { + var clone = new lunr.SortedSet + + clone.elements = this.toArray() + clone.length = clone.elements.length + + return clone +} + +/** + * Creates a new lunr.SortedSet that contains the elements in the union + * of this set and the passed set. + * + * @param {lunr.SortedSet} otherSet The set to union with this set. + * @returns {lunr.SortedSet} + * @memberOf SortedSet + */ +lunr.SortedSet.prototype.union = function (otherSet) { + var longSet, shortSet, unionSet + + if (this.length >= otherSet.length) { + longSet = this, shortSet = otherSet + } else { + longSet = otherSet, shortSet = this + } + + unionSet = longSet.clone() + + unionSet.add.apply(unionSet, shortSet.toArray()) + + return unionSet +} + +/** + * Returns a representation of the sorted set ready for serialisation. + * + * @returns {Array} + * @memberOf SortedSet + */ +lunr.SortedSet.prototype.toJSON = function () { + return this.toArray() +} +/*! + * lunr.Index + * Copyright (C) 2014 Oliver Nightingale + */ + +/** + * lunr.Index is object that manages a search index. It contains the indexes + * and stores all the tokens and document lookups. It also provides the main + * user facing API for the library. + * + * @constructor + */ +lunr.Index = function () { + this._fields = [] + this._ref = 'id' + this.pipeline = new lunr.Pipeline + this.documentStore = new lunr.Store + this.tokenStore = new lunr.TokenStore + this.corpusTokens = new lunr.SortedSet + this.eventEmitter = new lunr.EventEmitter + + this._idfCache = {} + + this.on('add', 'remove', 'update', (function () { + this._idfCache = {} + }).bind(this)) +} + +/** + * Bind a handler to events being emitted by the index. + * + * The handler can be bound to many events at the same time. + * + * @param {String} [eventName] The name(s) of events to bind the function to. + * @param {Function} handler The serialised set to load. + * @memberOf Index + */ +lunr.Index.prototype.on = function () { + var args = Array.prototype.slice.call(arguments) + return this.eventEmitter.addListener.apply(this.eventEmitter, args) +} + +/** + * Removes a handler from an event being emitted by the index. + * + * @param {String} eventName The name of events to remove the function from. + * @param {Function} handler The serialised set to load. + * @memberOf Index + */ +lunr.Index.prototype.off = function (name, fn) { + return this.eventEmitter.removeListener(name, fn) +} + +/** + * Loads a previously serialised index. + * + * Issues a warning if the index being imported was serialised + * by a different version of lunr. + * + * @param {Object} serialisedData The serialised set to load. + * @returns {lunr.Index} + * @memberOf Index + */ +lunr.Index.load = function (serialisedData) { + if (serialisedData.version !== lunr.version) { + lunr.utils.warn('version mismatch: current ' + lunr.version + ' importing ' + serialisedData.version) + } + + var idx = new this + + idx._fields = serialisedData.fields + idx._ref = serialisedData.ref + + idx.documentStore = lunr.Store.load(serialisedData.documentStore) + idx.tokenStore = lunr.TokenStore.load(serialisedData.tokenStore) + idx.corpusTokens = lunr.SortedSet.load(serialisedData.corpusTokens) + idx.pipeline = lunr.Pipeline.load(serialisedData.pipeline) + + return idx +} + +/** + * Adds a field to the list of fields that will be searchable within documents + * in the index. + * + * An optional boost param can be passed to affect how much tokens in this field + * rank in search results, by default the boost value is 1. + * + * Fields should be added before any documents are added to the index, fields + * that are added after documents are added to the index will only apply to new + * documents added to the index. + * + * @param {String} fieldName The name of the field within the document that + * should be indexed + * @param {Number} boost An optional boost that can be applied to terms in this + * field. + * @returns {lunr.Index} + * @memberOf Index + */ +lunr.Index.prototype.field = function (fieldName, opts) { + var opts = opts || {}, + field = { name: fieldName, boost: opts.boost || 1 } + + this._fields.push(field) + return this +} + +/** + * Sets the property used to uniquely identify documents added to the index, + * by default this property is 'id'. + * + * This should only be changed before adding documents to the index, changing + * the ref property without resetting the index can lead to unexpected results. + * + * @param {String} refName The property to use to uniquely identify the + * documents in the index. + * @param {Boolean} emitEvent Whether to emit add events, defaults to true + * @returns {lunr.Index} + * @memberOf Index + */ +lunr.Index.prototype.ref = function (refName) { + this._ref = refName + return this +} + +/** + * Add a document to the index. + * + * This is the way new documents enter the index, this function will run the + * fields from the document through the index's pipeline and then add it to + * the index, it will then show up in search results. + * + * An 'add' event is emitted with the document that has been added and the index + * the document has been added to. This event can be silenced by passing false + * as the second argument to add. + * + * @param {Object} doc The document to add to the index. + * @param {Boolean} emitEvent Whether or not to emit events, default true. + * @memberOf Index + */ +lunr.Index.prototype.add = function (doc, emitEvent) { + var docTokens = {}, + allDocumentTokens = new lunr.SortedSet, + docRef = doc[this._ref], + emitEvent = emitEvent === undefined ? true : emitEvent + + this._fields.forEach(function (field) { + var fieldTokens = this.pipeline.run(lunr.tokenizer(doc[field.name])) + + docTokens[field.name] = fieldTokens + lunr.SortedSet.prototype.add.apply(allDocumentTokens, fieldTokens) + }, this) + + this.documentStore.set(docRef, allDocumentTokens) + lunr.SortedSet.prototype.add.apply(this.corpusTokens, allDocumentTokens.toArray()) + + for (var i = 0; i < allDocumentTokens.length; i++) { + var token = allDocumentTokens.elements[i] + var tf = this._fields.reduce(function (memo, field) { + var fieldLength = docTokens[field.name].length + + if (!fieldLength) return memo + + var tokenCount = docTokens[field.name].filter(function (t) { return t === token }).length + + return memo + (tokenCount / fieldLength * field.boost) + }, 0) + + this.tokenStore.add(token, { ref: docRef, tf: tf }) + }; + + if (emitEvent) this.eventEmitter.emit('add', doc, this) +} + +/** + * Removes a document from the index. + * + * To make sure documents no longer show up in search results they can be + * removed from the index using this method. + * + * The document passed only needs to have the same ref property value as the + * document that was added to the index, they could be completely different + * objects. + * + * A 'remove' event is emitted with the document that has been removed and the index + * the document has been removed from. This event can be silenced by passing false + * as the second argument to remove. + * + * @param {Object} doc The document to remove from the index. + * @param {Boolean} emitEvent Whether to emit remove events, defaults to true + * @memberOf Index + */ +lunr.Index.prototype.remove = function (doc, emitEvent) { + var docRef = doc[this._ref], + emitEvent = emitEvent === undefined ? true : emitEvent + + if (!this.documentStore.has(docRef)) return + + var docTokens = this.documentStore.get(docRef) + + this.documentStore.remove(docRef) + + docTokens.forEach(function (token) { + this.tokenStore.remove(token, docRef) + }, this) + + if (emitEvent) this.eventEmitter.emit('remove', doc, this) +} + +/** + * Updates a document in the index. + * + * When a document contained within the index gets updated, fields changed, + * added or removed, to make sure it correctly matched against search queries, + * it should be updated in the index. + * + * This method is just a wrapper around `remove` and `add` + * + * An 'update' event is emitted with the document that has been updated and the index. + * This event can be silenced by passing false as the second argument to update. Only + * an update event will be fired, the 'add' and 'remove' events of the underlying calls + * are silenced. + * + * @param {Object} doc The document to update in the index. + * @param {Boolean} emitEvent Whether to emit update events, defaults to true + * @see Index.prototype.remove + * @see Index.prototype.add + * @memberOf Index + */ +lunr.Index.prototype.update = function (doc, emitEvent) { + var emitEvent = emitEvent === undefined ? true : emitEvent + + this.remove(doc, false) + this.add(doc, false) + + if (emitEvent) this.eventEmitter.emit('update', doc, this) +} + +/** + * Calculates the inverse document frequency for a token within the index. + * + * @param {String} token The token to calculate the idf of. + * @see Index.prototype.idf + * @private + * @memberOf Index + */ +lunr.Index.prototype.idf = function (term) { + var cacheKey = "@" + term + if (Object.prototype.hasOwnProperty.call(this._idfCache, cacheKey)) return this._idfCache[cacheKey] + + var documentFrequency = this.tokenStore.count(term), + idf = 1 + + if (documentFrequency > 0) { + idf = 1 + Math.log(this.tokenStore.length / documentFrequency) + } + + return this._idfCache[cacheKey] = idf +} + +/** + * Searches the index using the passed query. + * + * Queries should be a string, multiple words are allowed and will lead to an + * AND based query, e.g. `idx.search('foo bar')` will run a search for + * documents containing both 'foo' and 'bar'. + * + * All query tokens are passed through the same pipeline that document tokens + * are passed through, so any language processing involved will be run on every + * query term. + * + * Each query term is expanded, so that the term 'he' might be expanded to + * 'hello' and 'help' if those terms were already included in the index. + * + * Matching documents are returned as an array of objects, each object contains + * the matching document ref, as set for this index, and the similarity score + * for this document against the query. + * + * @param {String} query The query to search the index with. + * @returns {Object} + * @see Index.prototype.idf + * @see Index.prototype.documentVector + * @memberOf Index + */ +lunr.Index.prototype.search = function (query) { + var queryTokens = this.pipeline.run(lunr.tokenizer(query)), + queryVector = new lunr.Vector, + documentSets = [], + fieldBoosts = this._fields.reduce(function (memo, f) { return memo + f.boost }, 0) + + var hasSomeToken = queryTokens.some(function (token) { + return this.tokenStore.has(token) + }, this) + + if (!hasSomeToken) return [] + + queryTokens + .forEach(function (token, i, tokens) { + var tf = 1 / tokens.length * this._fields.length * fieldBoosts, + self = this + + var set = this.tokenStore.expand(token).reduce(function (memo, key) { + var pos = self.corpusTokens.indexOf(key), + idf = self.idf(key), + similarityBoost = 1, + set = new lunr.SortedSet + + // if the expanded key is not an exact match to the token then + // penalise the score for this key by how different the key is + // to the token. + if (key !== token) { + var diff = Math.max(3, key.length - token.length) + similarityBoost = 1 / Math.log(diff) + } + + // calculate the query tf-idf score for this token + // applying an similarityBoost to ensure exact matches + // these rank higher than expanded terms + if (pos > -1) queryVector.insert(pos, tf * idf * similarityBoost) + + // add all the documents that have this key into a set + Object.keys(self.tokenStore.get(key)).forEach(function (ref) { set.add(ref) }) + + return memo.union(set) + }, new lunr.SortedSet) + + documentSets.push(set) + }, this) + + var documentSet = documentSets.reduce(function (memo, set) { + return memo.intersect(set) + }) + + return documentSet + .map(function (ref) { + return { ref: ref, score: queryVector.similarity(this.documentVector(ref)) } + }, this) + .sort(function (a, b) { + return b.score - a.score + }) +} + +/** + * Generates a vector containing all the tokens in the document matching the + * passed documentRef. + * + * The vector contains the tf-idf score for each token contained in the + * document with the passed documentRef. The vector will contain an element + * for every token in the indexes corpus, if the document does not contain that + * token the element will be 0. + * + * @param {Object} documentRef The ref to find the document with. + * @returns {lunr.Vector} + * @private + * @memberOf Index + */ +lunr.Index.prototype.documentVector = function (documentRef) { + var documentTokens = this.documentStore.get(documentRef), + documentTokensLength = documentTokens.length, + documentVector = new lunr.Vector + + for (var i = 0; i < documentTokensLength; i++) { + var token = documentTokens.elements[i], + tf = this.tokenStore.get(token)[documentRef].tf, + idf = this.idf(token) + + documentVector.insert(this.corpusTokens.indexOf(token), tf * idf) + }; + + return documentVector +} + +/** + * Returns a representation of the index ready for serialisation. + * + * @returns {Object} + * @memberOf Index + */ +lunr.Index.prototype.toJSON = function () { + return { + version: lunr.version, + fields: this._fields, + ref: this._ref, + documentStore: this.documentStore.toJSON(), + tokenStore: this.tokenStore.toJSON(), + corpusTokens: this.corpusTokens.toJSON(), + pipeline: this.pipeline.toJSON() + } +} + +/** + * Applies a plugin to the current index. + * + * A plugin is a function that is called with the index as its context. + * Plugins can be used to customise or extend the behaviour the index + * in some way. A plugin is just a function, that encapsulated the custom + * behaviour that should be applied to the index. + * + * The plugin function will be called with the index as its argument, additional + * arguments can also be passed when calling use. The function will be called + * with the index as its context. + * + * Example: + * + * var myPlugin = function (idx, arg1, arg2) { + * // `this` is the index to be extended + * // apply any extensions etc here. + * } + * + * var idx = lunr(function () { + * this.use(myPlugin, 'arg1', 'arg2') + * }) + * + * @param {Function} plugin The plugin to apply. + * @memberOf Index + */ +lunr.Index.prototype.use = function (plugin) { + var args = Array.prototype.slice.call(arguments, 1) + args.unshift(this) + plugin.apply(this, args) +} +/*! + * lunr.Store + * Copyright (C) 2014 Oliver Nightingale + */ + +/** + * lunr.Store is a simple key-value store used for storing sets of tokens for + * documents stored in index. + * + * @constructor + * @module + */ +lunr.Store = function () { + this.store = {} + this.length = 0 +} + +/** + * Loads a previously serialised store + * + * @param {Object} serialisedData The serialised store to load. + * @returns {lunr.Store} + * @memberOf Store + */ +lunr.Store.load = function (serialisedData) { + var store = new this + + store.length = serialisedData.length + store.store = Object.keys(serialisedData.store).reduce(function (memo, key) { + memo[key] = lunr.SortedSet.load(serialisedData.store[key]) + return memo + }, {}) + + return store +} + +/** + * Stores the given tokens in the store against the given id. + * + * @param {Object} id The key used to store the tokens against. + * @param {Object} tokens The tokens to store against the key. + * @memberOf Store + */ +lunr.Store.prototype.set = function (id, tokens) { + this.store[id] = tokens + this.length = Object.keys(this.store).length +} + +/** + * Retrieves the tokens from the store for a given key. + * + * @param {Object} id The key to lookup and retrieve from the store. + * @returns {Object} + * @memberOf Store + */ +lunr.Store.prototype.get = function (id) { + return this.store[id] +} + +/** + * Checks whether the store contains a key. + * + * @param {Object} id The id to look up in the store. + * @returns {Boolean} + * @memberOf Store + */ +lunr.Store.prototype.has = function (id) { + return id in this.store +} + +/** + * Removes the value for a key in the store. + * + * @param {Object} id The id to remove from the store. + * @memberOf Store + */ +lunr.Store.prototype.remove = function (id) { + if (!this.has(id)) return + + delete this.store[id] + this.length-- +} + +/** + * Returns a representation of the store ready for serialisation. + * + * @returns {Object} + * @memberOf Store + */ +lunr.Store.prototype.toJSON = function () { + return { + store: this.store, + length: this.length + } +} + +/*! + * lunr.stemmer + * Copyright (C) 2014 Oliver Nightingale + * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt + */ + +/** + * lunr.stemmer is an english language stemmer, this is a JavaScript + * implementation of the PorterStemmer taken from http://tartaurs.org/~martin + * + * @module + * @param {String} str The string to stem + * @returns {String} + * @see lunr.Pipeline + */ +lunr.stemmer = (function(){ + var step2list = { + "ational" : "ate", + "tional" : "tion", + "enci" : "ence", + "anci" : "ance", + "izer" : "ize", + "bli" : "ble", + "alli" : "al", + "entli" : "ent", + "eli" : "e", + "ousli" : "ous", + "ization" : "ize", + "ation" : "ate", + "ator" : "ate", + "alism" : "al", + "iveness" : "ive", + "fulness" : "ful", + "ousness" : "ous", + "aliti" : "al", + "iviti" : "ive", + "biliti" : "ble", + "logi" : "log" + }, + + step3list = { + "icate" : "ic", + "ative" : "", + "alize" : "al", + "iciti" : "ic", + "ical" : "ic", + "ful" : "", + "ness" : "" + }, + + c = "[^aeiou]", // consonant + v = "[aeiouy]", // vowel + C = c + "[^aeiouy]*", // consonant sequence + V = v + "[aeiou]*", // vowel sequence + + mgr0 = "^(" + C + ")?" + V + C, // [C]VC... is m>0 + meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$", // [C]VC[V] is m=1 + mgr1 = "^(" + C + ")?" + V + C + V + C, // [C]VCVC... is m>1 + s_v = "^(" + C + ")?" + v; // vowel in stem + + return function (w) { + var stem, + suffix, + firstch, + re, + re2, + re3, + re4; + + if (w.length < 3) { return w; } + + firstch = w.substr(0,1); + if (firstch == "y") { + w = firstch.toUpperCase() + w.substr(1); + } + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) { w = w.replace(re,"$1$2"); } + else if (re2.test(w)) { w = w.replace(re2,"$1$2"); } + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) { w = w + "e"; } + else if (re3.test(w)) { re = /.$/; w = w.replace(re,""); } + else if (re4.test(w)) { w = w + "e"; } + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) { w = stem + "i"; } + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) { + w = stem + step2list[suffix]; + } + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) { + w = stem + step3list[suffix]; + } + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) { + w = stem; + } + } else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) { + w = stem; + } + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) { + w = stem; + } + } + + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + + if (firstch == "y") { + w = firstch.toLowerCase() + w.substr(1); + } + + return w; + } +})(); + +lunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer') +/*! + * lunr.stopWordFilter + * Copyright (C) 2014 Oliver Nightingale + */ + +/** + * lunr.stopWordFilter is an English language stop word list filter, any words + * contained in the list will not be passed through the filter. + * + * This is intended to be used in the Pipeline. If the token does not pass the + * filter then undefined will be returned. + * + * @module + * @param {String} token The token to pass through the filter + * @returns {String} + * @see lunr.Pipeline + */ +lunr.stopWordFilter = function (token) { + if (lunr.stopWordFilter.stopWords.indexOf(token) === -1) return token +} + +lunr.stopWordFilter.stopWords = new lunr.SortedSet +lunr.stopWordFilter.stopWords.length = 119 +lunr.stopWordFilter.stopWords.elements = [ + "", + "a", + "able", + "about", + "across", + "after", + "all", + "almost", + "also", + "am", + "among", + "an", + "and", + "any", + "are", + "as", + "at", + "be", + "because", + "been", + "but", + "by", + "can", + "cannot", + "could", + "dear", + "did", + "do", + "does", + "either", + "else", + "ever", + "every", + "for", + "from", + "get", + "got", + "had", + "has", + "have", + "he", + "her", + "hers", + "him", + "his", + "how", + "however", + "i", + "if", + "in", + "into", + "is", + "it", + "its", + "just", + "least", + "let", + "like", + "likely", + "may", + "me", + "might", + "most", + "must", + "my", + "neither", + "no", + "nor", + "not", + "of", + "off", + "often", + "on", + "only", + "or", + "other", + "our", + "own", + "rather", + "said", + "say", + "says", + "she", + "should", + "since", + "so", + "some", + "than", + "that", + "the", + "their", + "them", + "then", + "there", + "these", + "they", + "this", + "tis", + "to", + "too", + "twas", + "us", + "wants", + "was", + "we", + "were", + "what", + "when", + "where", + "which", + "while", + "who", + "whom", + "why", + "will", + "with", + "would", + "yet", + "you", + "your" +] + +lunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter') +/*! + * lunr.trimmer + * Copyright (C) 2014 Oliver Nightingale + */ + +/** + * lunr.trimmer is a pipeline function for trimming non word + * characters from the begining and end of tokens before they + * enter the index. + * + * This implementation may not work correctly for non latin + * characters and should either be removed or adapted for use + * with languages with non-latin characters. + * + * @module + * @param {String} token The token to pass through the filter + * @returns {String} + * @see lunr.Pipeline + */ +lunr.trimmer = function (token) { + return token + .replace(/^\W+/, '') + .replace(/\W+$/, '') +} + +lunr.Pipeline.registerFunction(lunr.trimmer, 'trimmer') +/*! + * lunr.stemmer + * Copyright (C) 2014 Oliver Nightingale + * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt + */ + +/** + * lunr.TokenStore is used for efficient storing and lookup of the reverse + * index of token to document ref. + * + * @constructor + */ +lunr.TokenStore = function () { + this.root = { docs: {} } + this.length = 0 +} + +/** + * Loads a previously serialised token store + * + * @param {Object} serialisedData The serialised token store to load. + * @returns {lunr.TokenStore} + * @memberOf TokenStore + */ +lunr.TokenStore.load = function (serialisedData) { + var store = new this + + store.root = serialisedData.root + store.length = serialisedData.length + + return store +} + +/** + * Adds a new token doc pair to the store. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to store the doc under + * @param {Object} doc The doc to store against the token + * @param {Object} root An optional node at which to start looking for the + * correct place to enter the doc, by default the root of this lunr.TokenStore + * is used. + * @memberOf TokenStore + */ +lunr.TokenStore.prototype.add = function (token, doc, root) { + var root = root || this.root, + key = token[0], + rest = token.slice(1) + + if (!(key in root)) root[key] = {docs: {}} + + if (rest.length === 0) { + root[key].docs[doc.ref] = doc + this.length += 1 + return + } else { + return this.add(rest, doc, root[key]) + } +} + +/** + * Checks whether this key is contained within this lunr.TokenStore. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to check for + * @param {Object} root An optional node at which to start + * @memberOf TokenStore + */ +lunr.TokenStore.prototype.has = function (token) { + if (!token) return false + + var node = this.root + + for (var i = 0; i < token.length; i++) { + if (!node[token[i]]) return false + + node = node[token[i]] + } + + return true +} + +/** + * Retrieve a node from the token store for a given token. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to get the node for. + * @param {Object} root An optional node at which to start. + * @returns {Object} + * @see TokenStore.prototype.get + * @memberOf TokenStore + */ +lunr.TokenStore.prototype.getNode = function (token) { + if (!token) return {} + + var node = this.root + + for (var i = 0; i < token.length; i++) { + if (!node[token[i]]) return {} + + node = node[token[i]] + } + + return node +} + +/** + * Retrieve the documents for a node for the given token. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to get the documents for. + * @param {Object} root An optional node at which to start. + * @returns {Object} + * @memberOf TokenStore + */ +lunr.TokenStore.prototype.get = function (token, root) { + return this.getNode(token, root).docs || {} +} + +lunr.TokenStore.prototype.count = function (token, root) { + return Object.keys(this.get(token, root)).length +} + +/** + * Remove the document identified by ref from the token in the store. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to get the documents for. + * @param {String} ref The ref of the document to remove from this token. + * @param {Object} root An optional node at which to start. + * @returns {Object} + * @memberOf TokenStore + */ +lunr.TokenStore.prototype.remove = function (token, ref) { + if (!token) return + var node = this.root + + for (var i = 0; i < token.length; i++) { + if (!(token[i] in node)) return + node = node[token[i]] + } + + delete node.docs[ref] +} + +/** + * Find all the possible suffixes of the passed token using tokens + * currently in the store. + * + * @param {String} token The token to expand. + * @returns {Array} + * @memberOf TokenStore + */ +lunr.TokenStore.prototype.expand = function (token, memo) { + var root = this.getNode(token), + docs = root.docs || {}, + memo = memo || [] + + if (Object.keys(docs).length) memo.push(token) + + Object.keys(root) + .forEach(function (key) { + if (key === 'docs') return + + memo.concat(this.expand(token + key, memo)) + }, this) + + return memo +} + +/** + * Returns a representation of the token store ready for serialisation. + * + * @returns {Object} + * @memberOf TokenStore + */ +lunr.TokenStore.prototype.toJSON = function () { + return { + root: this.root, + length: this.length + } +} + + + /** + * export the module via AMD, CommonnJS or as a browser global + * Export code from https://github.com/umdjs/umd/blob/master/returnExports.js + */ + ;(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(factory) + } else if (typeof exports === 'object') { + /** + * Node. Does not work with strict CommonJS, but + * only CommonJS-like enviroments that support module.exports, + * like Node. + */ + module.exports = factory() + } else { + // Browser globals (root is window) + root.lunr = factory() + } + }(this, function () { + /** + * Just return a value to define the module export. + * This example returns an object, but the module + * can return a function as the exported value. + */ + return lunr + })) +})() diff --git a/source/layouts/layout.erb b/source/layouts/layout.erb index bcceb15..0f98aaf 100644 --- a/source/layouts/layout.erb +++ b/source/layouts/layout.erb @@ -13,95 +13,65 @@ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. %> +<% language_tabs = current_page.data.language_tabs %> - - - - <%= current_page.data.title || "API Documentation" %> - <%= stylesheet_link_tag "screen", media: 'screen' %> - <%= stylesheet_link_tag "print", media: 'print' %> - + <%= stylesheet_link_tag :screen, media: :screen %> + <%= stylesheet_link_tag :print, media: :print %> + <%= javascript_include_tag "all" %> - + <% if language_tabs %> + + <% end %>
<%= image_tag "logo.png" %> +
<% if current_page.data.toc_footers %> - + <% end %>
+
<%= yield %> - <% if current_page.data.includes %> - <% current_page.data.includes.each do |include| %> - <%= partial "includes/#{include}" %> - <% end %> + <% current_page.data.includes && current_page.data.includes.each do |include| %> + <%= partial "includes/#{include}" %> <% end %>
-
- <% if current_page.data.language_tabs %> - <% current_page.data.language_tabs.each do |lang| %> + <% if language_tabs %> +
+ <% language_tabs.each do |lang| %> <% if lang.is_a? Hash %> - <%= lang.values[0] %> + <%= lang.values.first %> <% else %> <%= lang %> <% end %> <% end %> - <% end %> -
+
+ <% end %>
- \ No newline at end of file + diff --git a/source/stylesheets/icon-font.scss b/source/stylesheets/icon-font.scss index a6f4082..66504b5 100644 --- a/source/stylesheets/icon-font.scss +++ b/source/stylesheets/icon-font.scss @@ -47,3 +47,7 @@ @extend %icon; content: "\e606"; } +%icon-search { + @extend %icon; + content: "\e607"; +} diff --git a/source/stylesheets/screen.css.scss b/source/stylesheets/screen.css.scss index debca3d..e061c7f 100644 --- a/source/stylesheets/screen.css.scss +++ b/source/stylesheets/screen.css.scss @@ -33,6 +33,7 @@ html, body { -moz-osx-font-smoothing: grayscale; @extend %default-font; background-color: $main-bg; + height: 100%; } //////////////////////////////////////////////////////////////////////////////// @@ -54,13 +55,37 @@ html, body { // This is the logo at the top of the ToC &>img { display: block; - margin-bottom: $logo-margin; + } + + &>.search { + position: relative; + + input { + background: $nav-bg; + border-width: 0 0 1px 0; + border-color: $search-box-border-color; + padding: 6px 0 6px 20px; + box-sizing: border-box; + margin: 10px 15px; + width: $nav-width - 30; + outline: none; + color: $nav-text; + letter-spacing: 0.07em; + } + + &:before { + @extend %icon-search; + position: absolute; + top: 16px; + left: 15px; + color: $nav-text; + } } .tocify-item>a, .toc-footer li { padding: 0 $nav-padding 0 $nav-padding; - display:block; - overflow-x:hidden; + display: block; + overflow-x: hidden; white-space: nowrap; text-overflow: ellipsis; } @@ -152,6 +177,7 @@ html, body { position: relative; z-index: 10; background-color: $main-bg; + min-height: 100%; padding-bottom: 1px; // prevent margin overflow @@ -213,180 +239,206 @@ html, body { position: relative; z-index: 30; + section { + &>h1, &>h2, &>h3, &>p, &>table, &>ul, &>ol, &>aside, &>dl { + margin-right: $examples-width; + padding: 0 $main-padding; + @include box-sizing(border-box); + display: block; + @include text-shadow($main-embossed-text-shadow); + } - &>h1, &>h2, &>h3, &>p, &>table, &>ul, &>ol, &>aside, &>dl { - margin-right: $examples-width; - padding: 0 $main-padding; - @include box-sizing(border-box); - display: block; - @include text-shadow($main-embossed-text-shadow); - } + &>ul, &>ol { + padding-left: $main-padding + 15px; + } - &>ul, &>ol { - padding-left: $main-padding + 15px; - } + // the div is the tocify hidden div for placeholding stuff + &>h1, &>h2, &>div { + clear:both; + } - // the div is the tocify hidden div for placeholding stuff - &>h1, &>h2, &>div { - clear:both; - } + h1 { + @extend %header-font; + font-size: 30px; + padding-top: 0.5em; + padding-bottom: 0.5em; + border-bottom: 1px solid #ccc; + margin-top: 2em; + margin-bottom: $h1-margin-bottom; + border-top: 1px solid #ddd; + @include background-image( + linear-gradient(top, #fff, #f9f9f9) + ); + } - h1 { - @extend %header-font; - font-size: 30px; - padding-top: 0.5em; - padding-bottom: 0.5em; - border-bottom: 1px solid #ccc; - margin-top: 2em; - margin-bottom: $h1-margin-bottom; - border-top: 1px solid #ddd; - @include background-image( - linear-gradient(top, #fff, #f9f9f9) - ); - } + // The header at the very top of the page + // shouldn't have top margin. + // (the div is because of tocify) + h1:first-child, div:first-child + h1 { + margin-top: 0; + } - // The header at the very top of the page - // shouldn't have top margin. - // (the div is because of tocify) - h1:first-child, div:first-child + h1 { - margin-top: 0; - } + h2 { + @extend %header-font; + font-size: 20px; + margin-top: 4em; + margin-bottom: 0; + border-top: 1px solid #ccc; + padding-top: 1.2em; + padding-bottom: 1.2em; + @include background-image( + linear-gradient(top, rgba(#fff,0.4), rgba(#fff, 0)) + ); + } - h2 { - @extend %header-font; - font-size: 20px; - margin-top: 4em; - margin-bottom: 0; - border-top: 1px solid #ccc; - padding-top: 1.2em; - padding-bottom: 1.2em; - @include background-image( - linear-gradient(top, rgba(#fff,0.4), rgba(#fff, 0)) - ); - } + // h2s right after h1s should bump right up + // against the h1s. + h1 + h2, h1 + div + h2 { + margin-top: $h1-margin-bottom * -1; + border-top: none; + } - // h2s right after h1s should bump right up - // against the h1s. - h1 + h2, h1 + div + h2 { - margin-top: $h1-margin-bottom * -1; - border-top: none; - } + h3 { + @extend %header-font; + font-size: 12px; + margin-top: 2.5em; + margin-bottom: 0.8em; + text-transform: uppercase; + } - h3 { - @extend %header-font; - font-size: 12px; - margin-top: 2.5em; - margin-bottom: 0.8em; - text-transform: uppercase; - } + hr { + margin: 2em 0; + border-top: 2px solid $examples-bg; + border-bottom: 2px solid $main-bg; + } - hr { - margin: 2em 0; - border-top: 2px solid $examples-bg; - border-bottom: 2px solid $main-bg; - } + table { + margin-bottom: 1em; + overflow: auto; + th,td { + text-align: left; + vertical-align: top; + line-height: 1.6; + } - table { - margin-bottom: 1em; - overflow: auto; - th,td { - text-align: left; - vertical-align: top; + th { + padding: 5px 10px; + border-bottom: 1px solid #ccc; + vertical-align: bottom; + } + + td { + padding: 10px; + } + + tr:last-child { + border-bottom: 1px solid #ccc; + } + + tr:nth-child(odd)>td { + background-color: lighten($main-bg,4.2%); + } + + tr:nth-child(even)>td { + background-color: lighten($main-bg,2.4%); + } + } + + dt { + font-weight: bold; + } + + dd { + margin-left: 15px; + } + + p, li, dt, dd { line-height: 1.6; + margin-top: 0; } - th { - padding: 5px 10px; - border-bottom: 1px solid #ccc; - vertical-align: bottom; + img { + max-width: 100%; } - td { - padding: 10px; + code { + background-color: rgba(0,0,0,0.05); + padding: 3px; + border-radius: 3px; + @extend %break-words; + @extend %code-font; } - tr:last-child { - border-bottom: 1px solid #ccc; + aside { + padding-top: 1em; + padding-bottom: 1em; + text-shadow: 0 1px 0 lighten($aside-notice-bg, 15%); + margin-top: 1.5em; + margin-bottom: 1.5em; + background: $aside-notice-bg; + line-height: 1.6; + + &.warning { + background-color: $aside-warning-bg; + text-shadow: 0 1px 0 lighten($aside-warning-bg, 15%); + } + + &.success { + background-color: $aside-success-bg; + text-shadow: 0 1px 0 lighten($aside-success-bg, 15%); + } } - tr:nth-child(odd)>td { - background-color: lighten($main-bg,4.2%); + aside:before { + vertical-align: middle; + padding-right: 0.5em; + font-size: 14px; } - tr:nth-child(even)>td { - background-color: lighten($main-bg,2.4%); + aside.notice:before { + @extend %icon-info-sign; + } + + aside.warning:before { + @extend %icon-exclamation-sign; + } + + aside.success:before { + @extend %icon-ok-sign; } } - dt { - font-weight: bold; + .search-highlight { + padding: 2px; + margin: -2px; + border-radius: 4px; + border: 1px solid #F7E633; + text-shadow: 1px 1px 0 #666; + @include background(linear-gradient(to bottom right, #F7E633 0%, #F1D32F 100%)); } - - dd { - margin-left: 15px; - } - - p, li, dt, dd { - line-height: 1.6; - margin-top: 0; - } - - img { - max-width: 100%; - } - - code { - background-color: rgba(0,0,0,0.05); - padding: 3px; - border-radius: 3px; - @extend %break-words; - @extend %code-font; - } - - aside { - padding-top: 1em; - padding-bottom: 1em; - text-shadow: 0 1px 0 lighten($aside-notice-bg, 15%); - margin-top: 1.5em; - margin-bottom: 1.5em; - background: $aside-notice-bg; - line-height: 1.6; - - &.warning { - background-color: $aside-warning-bg; - text-shadow: 0 1px 0 lighten($aside-warning-bg, 15%); - } - - &.success { - background-color: $aside-success-bg; - text-shadow: 0 1px 0 lighten($aside-success-bg, 15%); - } - } - - - aside.warning { - } - - aside:before { - vertical-align: middle; - padding-right: 0.5em; - font-size: 14px; - } - - aside.notice:before { - @extend %icon-info-sign; - } - - aside.warning:before { - @extend %icon-exclamation-sign; - } - - aside.success:before { - @extend %icon-ok-sign; - } - } +.search-info { + margin-top: 0; + min-height: 52px; + padding: 1em 1.75em; + font-size: 1.2em; + font-weight: bold; + text-shadow: 0 1px 0 lighten($search-notice-bg, 15%); + background: $search-notice-bg; + position: fixed; + z-index: 75; + display: none; + + @include box-sizing(border-box); + + &:before { + @extend %icon-search; + vertical-align: middle; + padding-right: 0.5em; + font-size: 1.2em; + } +} //////////////////////////////////////////////////////////////////////////////// // CODE SAMPLE STYLES @@ -431,4 +483,4 @@ html, body { border-bottom: 1px solid #404040; } } -} \ No newline at end of file +} diff --git a/source/stylesheets/variables.scss b/source/stylesheets/variables.scss index 5aab15b..6b9021a 100644 --- a/source/stylesheets/variables.scss +++ b/source/stylesheets/variables.scss @@ -37,6 +37,7 @@ $main-bg: #eaf2f6; $aside-notice-bg: #8fbcd4; $aside-warning-bg: #c97a7e; $aside-success-bg: #6ac174; +$search-notice-bg: #8fbcd4; // TEXT COLORS @@ -86,6 +87,7 @@ $nav-footer-border-color: #666; $nav-embossed-border-top: 1px solid #000; $nav-embossed-border-bottom: 1px solid #404040; $main-embossed-text-shadow: 0px 1px 0px #fff; +$search-box-border-color: #666; ////////////////////////////////////////////////////////////////////////////////