diff options
Diffstat (limited to 'static/js/searchElasticlunr.js')
| -rw-r--r-- | static/js/searchElasticlunr.js | 3201 |
1 files changed, 0 insertions, 3201 deletions
diff --git a/static/js/searchElasticlunr.js b/static/js/searchElasticlunr.js deleted file mode 100644 index 9ad09e1..0000000 --- a/static/js/searchElasticlunr.js +++ /dev/null @@ -1,3201 +0,0 @@ -/** - * elasticlunr - http://weixsong.github.io - * Lightweight full-text search engine in Javascript for browser search and offline search. - 0.9.5 - * - * Copyright (C) 2017 Oliver Nightingale - * Copyright (C) 2017 Wei Song - * MIT Licensed - * @license - */ -(function () { - /*! - * elasticlunr.js - * Copyright (C) 2017 Oliver Nightingale - * Copyright (C) 2017 Wei Song - */ - - /** - * Convenience function for instantiating a new elasticlunr 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: - * - * 1. elasticlunr.trimmer - trim non-word character - * 2. elasticlunr.StopWordFilter - filters out any stop words before they enter the - * index - * 3. elasticlunr.stemmer - stems the tokens before entering the index. - * - * - * Example: - * - * var idx = elasticlunr(function () { - * this.addField('id'); - * this.addField('title'); - * this.addField('body'); - * - * //this.setRef('id'); // default ref is 'id' - * - * this.pipeline.add(function () { - * // some custom pipeline function - * }); - * }); - * - * idx.addDoc({ - * id: 1, - * title: 'Oracle released database 12g', - * body: 'Yestaday, Oracle has released their latest database, named 12g, more robust. this product will increase Oracle profit.' - * }); - * - * idx.addDoc({ - * id: 2, - * title: 'Oracle released annual profit report', - * body: 'Yestaday, Oracle has released their annual profit report of 2015, total profit is 12.5 Billion.' - * }); - * - * # simple search - * idx.search('oracle database'); - * - * # search with query-time boosting - * idx.search('oracle database', {fields: {title: {boost: 2}, body: {boost: 1}}}); - * - * @param {Function} config A function that will be called with the new instance - * of the elasticlunr.Index as both its context and first parameter. It can be used to - * customize the instance of new elasticlunr.Index. - * @namespace - * @module - * @return {elasticlunr.Index} - * - */ - const elasticlunr = function (config) { - const idx = new elasticlunr.Index(); - - idx.pipeline.add( - elasticlunr.trimmer, - elasticlunr.stopWordFilter, - elasticlunr.stemmer - ); - - if (config) config.call(idx, idx); - - return idx; - }; - - elasticlunr.version = '0.9.5'; - - // only used this to make elasticlunr.js compatible with lunr-languages - // this is a trick to define a global alias of elasticlunr - lunr = elasticlunr; - - /*! - * elasticlunr.utils - * Copyright (C) 2017 Oliver Nightingale - * Copyright (C) 2017 Wei Song - */ - - /** - * A namespace containing utils for the rest of the elasticlunr library - */ - elasticlunr.utils = {}; - - /** - * Print a warning message to the console. - * - * @param {String} message The message to be printed. - * @memberOf Utils - */ - elasticlunr.utils.warn = (function (global) { - return function (message) { - if (global.console && console.warn) { - console.warn(message); - } - }; - })(this); - - /** - * Convert an object to string. - * - * In the case of `null` and `undefined` the function returns - * an empty string, in all other cases the result of calling - * `toString` on the passed object is returned. - * - * @param {object} obj The object to convert to a string. - * @return {String} string representation of the passed object. - * @memberOf Utils - */ - elasticlunr.utils.toString = function (obj) { - if (obj === void 0 || obj === null) { - return ''; - } - - return obj.toString(); - }; - /*! - * elasticlunr.EventEmitter - * Copyright (C) 2017 Oliver Nightingale - * Copyright (C) 2017 Wei Song - */ - - /** - * elasticlunr.EventEmitter is an event emitter for elasticlunr. - * It manages adding and removing event handlers and triggering events and their handlers. - * - * Each event could has multiple corresponding functions, - * these functions will be called as the sequence that they are added into the event. - * - * @constructor - */ - elasticlunr.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} fn The function to call when an event is fired. - * @memberOf EventEmitter - */ - elasticlunr.EventEmitter.prototype.addListener = function () { - const args = Array.prototype.slice.call(arguments); - const fn = args.pop(); - const 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} fn The function to remove from an event. - * @memberOf EventEmitter - */ - elasticlunr.EventEmitter.prototype.removeListener = function (name, fn) { - if (!this.hasHandler(name)) return; - - const fnIndex = this.events[name].indexOf(fn); - if (fnIndex === -1) return; - - this.events[name].splice(fnIndex, 1); - - if (this.events[name].length === 0) delete this.events[name]; - }; - - /** - * Call all functions that bounded 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 - */ - elasticlunr.EventEmitter.prototype.emit = function (name) { - if (!this.hasHandler(name)) return; - - const args = Array.prototype.slice.call(arguments, 1); - - this.events[name].forEach(function (fn) { - fn.apply(undefined, args); - }, this); - }; - - /** - * Checks whether a handler has ever been stored against an event. - * - * @param {String} eventName The name of the event to check. - * @private - * @memberOf EventEmitter - */ - elasticlunr.EventEmitter.prototype.hasHandler = function (name) { - return name in this.events; - }; - /*! - * elasticlunr.tokenizer - * Copyright (C) 2017 Oliver Nightingale - * Copyright (C) 2017 Wei Song - */ - - /** - * A function for splitting a string into tokens. - * Currently English is supported as default. - * Uses `elasticlunr.tokenizer.seperator` to split strings, you could change - * the value of this property to set how you want strings are split into tokens. - * IMPORTANT: use elasticlunr.tokenizer.seperator carefully, if you are not familiar with - * text process, then you'd better not change it. - * - * @module - * @param {String} str The string that you want to tokenize. - * @see elasticlunr.tokenizer.seperator - * @return {Array} - */ - elasticlunr.tokenizer = function (str) { - if (!arguments.length || str === null || str === undefined) return []; - if (Array.isArray(str)) { - let arr = str.filter(function (token) { - if (token === null || token === undefined) { - return false; - } - - return true; - }); - - arr = arr.map(function (t) { - return elasticlunr.utils.toString(t).toLowerCase(); - }); - - let out = []; - arr.forEach(function (item) { - const tokens = item.split(elasticlunr.tokenizer.seperator); - out = out.concat(tokens); - }, this); - - return out; - } - - return str - .toString() - .trim() - .toLowerCase() - .split(elasticlunr.tokenizer.seperator); - }; - - /** - * Default string seperator. - */ - elasticlunr.tokenizer.defaultSeperator = /[\s-]+/; - - /** - * The sperator used to split a string into tokens. Override this property to change the behaviour of - * `elasticlunr.tokenizer` behaviour when tokenizing strings. By default this splits on whitespace and hyphens. - * - * @static - * @see elasticlunr.tokenizer - */ - elasticlunr.tokenizer.seperator = elasticlunr.tokenizer.defaultSeperator; - - /** - * Set up customized string seperator - * - * @param {Object} sep The customized seperator that you want to use to tokenize a string. - */ - elasticlunr.tokenizer.setSeperator = function (sep) { - if (sep !== null && sep !== undefined && typeof sep === 'object') { - elasticlunr.tokenizer.seperator = sep; - } - }; - - /** - * Reset string seperator - * - */ - elasticlunr.tokenizer.resetSeperator = function () { - elasticlunr.tokenizer.seperator = elasticlunr.tokenizer.defaultSeperator; - }; - - /** - * Get string seperator - * - */ - elasticlunr.tokenizer.getSeperator = function () { - return elasticlunr.tokenizer.seperator; - }; - /*! - * elasticlunr.Pipeline - * Copyright (C) 2017 Oliver Nightingale - * Copyright (C) 2017 Wei Song - */ - - /** - * elasticlunr.Pipelines maintain an ordered list of functions to be applied to - * both documents tokens and query tokens. - * - * An instance of elasticlunr.Index will contain a pipeline - * with a trimmer, a stop word filter, an English stemmer. Extra - * functions can be added before or after either of these functions or these - * default functions can be removed. - * - * When run the pipeline, it will call each function in turn. - * - * The output of the 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 elasticlunr.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 - */ - elasticlunr.Pipeline = function () { - this._queue = []; - }; - - elasticlunr.Pipeline.registeredFunctions = {}; - - /** - * Register a function in 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 register. - * @param {String} label The label to register this function with - * @memberOf Pipeline - */ - elasticlunr.Pipeline.registerFunction = function (fn, label) { - if (label in elasticlunr.Pipeline.registeredFunctions) { - elasticlunr.utils.warn( - 'Overwriting existing registered function: ' + label - ); - } - - fn.label = label; - elasticlunr.Pipeline.registeredFunctions[label] = fn; - }; - - /** - * Get a registered function in the pipeline. - * - * @param {String} label The label of registered function. - * @return {Function} - * @memberOf Pipeline - */ - elasticlunr.Pipeline.getRegisteredFunction = function (label) { - if (label in elasticlunr.Pipeline.registeredFunctions !== true) { - return null; - } - - return elasticlunr.Pipeline.registeredFunctions[label]; - }; - - /** - * Warns if the function is not registered as a Pipeline function. - * - * @param {Function} fn The function to check for. - * @private - * @memberOf Pipeline - */ - elasticlunr.Pipeline.warnIfFunctionNotRegistered = function (fn) { - const isRegistered = fn.label && fn.label in this.registeredFunctions; - - if (!isRegistered) { - elasticlunr.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 elasticlunr.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. - * @return {elasticlunr.Pipeline} - * @memberOf Pipeline - */ - elasticlunr.Pipeline.load = function (serialised) { - const pipeline = new elasticlunr.Pipeline(); - - serialised.forEach(function (fnName) { - const fn = elasticlunr.Pipeline.getRegisteredFunction(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 - */ - elasticlunr.Pipeline.prototype.add = function () { - const fns = Array.prototype.slice.call(arguments); - - fns.forEach(function (fn) { - elasticlunr.Pipeline.warnIfFunctionNotRegistered(fn); - this._queue.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. - * If existingFn is not found, throw an Exception. - * - * @param {Function} existingFn A function that already exists in the pipeline. - * @param {Function} newFn The new function to add to the pipeline. - * @memberOf Pipeline - */ - elasticlunr.Pipeline.prototype.after = function (existingFn, newFn) { - elasticlunr.Pipeline.warnIfFunctionNotRegistered(newFn); - - const pos = this._queue.indexOf(existingFn); - if (pos === -1) { - throw new Error('Cannot find existingFn'); - } - - this._queue.splice(pos + 1, 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. - * If existingFn is not found, throw an Exception. - * - * @param {Function} existingFn A function that already exists in the pipeline. - * @param {Function} newFn The new function to add to the pipeline. - * @memberOf Pipeline - */ - elasticlunr.Pipeline.prototype.before = function (existingFn, newFn) { - elasticlunr.Pipeline.warnIfFunctionNotRegistered(newFn); - - const pos = this._queue.indexOf(existingFn); - if (pos === -1) { - throw new Error('Cannot find existingFn'); - } - - this._queue.splice(pos, 0, newFn); - }; - - /** - * Removes a function from the pipeline. - * - * @param {Function} fn The function to remove from the pipeline. - * @memberOf Pipeline - */ - elasticlunr.Pipeline.prototype.remove = function (fn) { - const pos = this._queue.indexOf(fn); - if (pos === -1) { - return; - } - - this._queue.splice(pos, 1); - }; - - /** - * Runs the current list of functions that registered in the pipeline against the - * input tokens. - * - * @param {Array} tokens The tokens to run through the pipeline. - * @return {Array} - * @memberOf Pipeline - */ - elasticlunr.Pipeline.prototype.run = function (tokens) { - const out = []; - const tokenLength = tokens.length; - const pipelineLength = this._queue.length; - - for (let i = 0; i < tokenLength; i++) { - let token = tokens[i]; - - for (let j = 0; j < pipelineLength; j++) { - token = this._queue[j](token, i, tokens); - if (token === void 0 || token === null) break; - } - - if (token !== void 0 && token !== null) out.push(token); - } - - return out; - }; - - /** - * Resets the pipeline by removing any existing processors. - * - * @memberOf Pipeline - */ - elasticlunr.Pipeline.prototype.reset = function () { - this._queue = []; - }; - - /** - * Get the pipeline if user want to check the pipeline. - * - * @memberOf Pipeline - */ - elasticlunr.Pipeline.prototype.get = function () { - return this._queue; - }; - - /** - * Returns a representation of the pipeline ready for serialisation. - * Only serialize pipeline function's name. Not storing function, so when - * loading the archived JSON index file, corresponding pipeline function is - * added by registered function of elasticlunr.Pipeline.registeredFunctions - * - * Logs a warning if the function has not been registered. - * - * @return {Array} - * @memberOf Pipeline - */ - elasticlunr.Pipeline.prototype.toJSON = function () { - return this._queue.map(function (fn) { - elasticlunr.Pipeline.warnIfFunctionNotRegistered(fn); - return fn.label; - }); - }; - /*! - * elasticlunr.Index - * Copyright (C) 2017 Oliver Nightingale - * Copyright (C) 2017 Wei Song - */ - - /** - * elasticlunr.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 - */ - elasticlunr.Index = function () { - this._fields = []; - this._ref = 'id'; - this.pipeline = new elasticlunr.Pipeline(); - this.documentStore = new elasticlunr.DocumentStore(); - this.index = {}; - this.eventEmitter = new elasticlunr.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} fn The serialised set to load. - * @memberOf Index - */ - elasticlunr.Index.prototype.on = function () { - const 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} fn The serialised set to load. - * @memberOf Index - */ - elasticlunr.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 elasticlunr. - * - * @param {Object} serialisedData The serialised set to load. - * @return {elasticlunr.Index} - * @memberOf Index - */ - elasticlunr.Index.load = function (serialisedData) { - if (serialisedData.version !== elasticlunr.version) { - elasticlunr.utils.warn( - 'version mismatch: current ' + - elasticlunr.version + - ' importing ' + - serialisedData.version - ); - } - - const idx = new this(); - - idx._fields = serialisedData.fields; - idx._ref = serialisedData.ref; - idx.documentStore = elasticlunr.DocumentStore.load( - serialisedData.documentStore - ); - idx.pipeline = elasticlunr.Pipeline.load(serialisedData.pipeline); - idx.index = {}; - for (const field in serialisedData.index) { - idx.index[field] = elasticlunr.InvertedIndex.load( - serialisedData.index[field] - ); - } - - return idx; - }; - - /** - * Adds a field to the list of fields that will be searchable within documents in the index. - * - * Remember that inner index is build based on field, which means each field has one inverted index. - * - * 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 - * @return {elasticlunr.Index} - * @memberOf Index - */ - elasticlunr.Index.prototype.addField = function (fieldName) { - this._fields.push(fieldName); - this.index[fieldName] = new elasticlunr.InvertedIndex(); - 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 - * @return {elasticlunr.Index} - * @memberOf Index - */ - elasticlunr.Index.prototype.setRef = function (refName) { - this._ref = refName; - return this; - }; - - /** - * - * Set if the JSON format original documents are save into elasticlunr.DocumentStore - * - * Defaultly save all the original JSON documents. - * - * @param {Boolean} save Whether to save the original JSON documents. - * @return {elasticlunr.Index} - * @memberOf Index - */ - elasticlunr.Index.prototype.saveDocument = function (save) { - this.documentStore = new elasticlunr.DocumentStore(save); - return this; - }; - - /** - * Add a JSON format 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 JSON format document to add to the index. - * @param {Boolean} emitEvent Whether or not to emit events, default true. - * @memberOf Index - */ - elasticlunr.Index.prototype.addDoc = function (doc, emitEvent) { - if (!doc) return; - var emitEvent = emitEvent === undefined ? true : emitEvent; - - const docRef = doc[this._ref]; - - this.documentStore.addDoc(docRef, doc); - this._fields.forEach(function (field) { - const fieldTokens = this.pipeline.run(elasticlunr.tokenizer(doc[field])); - this.documentStore.addFieldLength(docRef, field, fieldTokens.length); - - const tokenCount = {}; - fieldTokens.forEach(function (token) { - if (token in tokenCount) tokenCount[token] += 1; - else tokenCount[token] = 1; - }, this); - - for (const token in tokenCount) { - let termFrequency = tokenCount[token]; - termFrequency = Math.sqrt(termFrequency); - this.index[field].addToken(token, { ref: docRef, tf: termFrequency }); - } - }, this); - - if (emitEvent) this.eventEmitter.emit('add', doc, this); - }; - - /** - * Removes a document from the index by doc ref. - * - * To make sure documents no longer show up in search results they can be - * removed from the index using this method. - * - * 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. - * - * If user setting DocumentStore not storing the documents, then remove doc by docRef is not allowed. - * - * @param {String|Integer} docRef The document ref to remove from the index. - * @param {Boolean} emitEvent Whether to emit remove events, defaults to true - * @memberOf Index - */ - elasticlunr.Index.prototype.removeDocByRef = function (docRef, emitEvent) { - if (!docRef) return; - if (this.documentStore.isDocStored() === false) { - return; - } - - if (!this.documentStore.hasDoc(docRef)) return; - const doc = this.documentStore.getDoc(docRef); - this.removeDoc(doc, false); - }; - - /** - * Removes a document from the index. - * This remove operation could work even the original doc is not store in the DocumentStore. - * - * To make sure documents no longer show up in search results they can be - * removed from the index using this method. - * - * 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 ref to remove from the index. - * @param {Boolean} emitEvent Whether to emit remove events, defaults to true - * @memberOf Index - */ - elasticlunr.Index.prototype.removeDoc = function (doc, emitEvent) { - if (!doc) return; - - var emitEvent = emitEvent === undefined ? true : emitEvent; - - const docRef = doc[this._ref]; - if (!this.documentStore.hasDoc(docRef)) return; - - this.documentStore.removeDoc(docRef); - - this._fields.forEach(function (field) { - const fieldTokens = this.pipeline.run(elasticlunr.tokenizer(doc[field])); - fieldTokens.forEach(function (token) { - this.index[field].removeToken(token, docRef); - }, this); - }, 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 - */ - elasticlunr.Index.prototype.updateDoc = function (doc, emitEvent) { - var emitEvent = emitEvent === undefined ? true : emitEvent; - - this.removeDocByRef(doc[this._ref], false); - this.addDoc(doc, false); - - if (emitEvent) this.eventEmitter.emit('update', doc, this); - }; - - /** - * Calculates the inverse document frequency for a token within the index of a field. - * - * @param {String} token The token to calculate the idf of. - * @param {String} field The field to compute idf. - * @see Index.prototype.idf - * @private - * @memberOf Index - */ - elasticlunr.Index.prototype.idf = function (term, field) { - const cacheKey = '@' + field + '/' + term; - if (Object.prototype.hasOwnProperty.call(this._idfCache, cacheKey)) - return this._idfCache[cacheKey]; - - const df = this.index[field].getDocFreq(term); - const idf = 1 + Math.log(this.documentStore.length / (df + 1)); - this._idfCache[cacheKey] = idf; - - return idf; - }; - - /** - * get fields of current index instance - * - * @return {Array} - */ - elasticlunr.Index.prototype.getFields = function () { - return this._fields.slice(); - }; - - /** - * Searches the index using the passed query. - * Queries should be a string, multiple words are allowed. - * - * If config is null, will search all fields defaultly, and lead to OR based query. - * If config is specified, will search specified with query time boosting. - * - * 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. - * @param {JSON} userConfig The user query config, JSON format. - * @return {Object} - * @see Index.prototype.idf - * @see Index.prototype.documentVector - * @memberOf Index - */ - elasticlunr.Index.prototype.search = function (query, userConfig) { - if (!query) return []; - if (typeof query === 'string') { - query = { any: query }; - } else { - query = JSON.parse(JSON.stringify(query)); - } - - let configStr = null; - if (userConfig != null) { - configStr = JSON.stringify(userConfig); - } - - const config = new elasticlunr.Configuration(configStr, this.getFields()).get(); - - const queryTokens = {}; - const queryFields = Object.keys(query); - - for (let i = 0; i < queryFields.length; i++) { - const key = queryFields[i]; - - queryTokens[key] = this.pipeline.run(elasticlunr.tokenizer(query[key])); - } - - const queryResults = {}; - - for (const field in config) { - const tokens = queryTokens[field] || queryTokens.any; - if (!tokens) { - continue; - } - - const fieldSearchResults = this.fieldSearch(tokens, field, config); - const fieldBoost = config[field].boost; - - for (var docRef in fieldSearchResults) { - fieldSearchResults[docRef] = fieldSearchResults[docRef] * fieldBoost; - } - - for (var docRef in fieldSearchResults) { - if (docRef in queryResults) { - queryResults[docRef] += fieldSearchResults[docRef]; - } else { - queryResults[docRef] = fieldSearchResults[docRef]; - } - } - } - - const results = []; - let result; - for (var docRef in queryResults) { - result = { ref: docRef, score: queryResults[docRef] }; - if (this.documentStore.hasDoc(docRef)) { - result.doc = this.documentStore.getDoc(docRef); - } - results.push(result); - } - - results.sort(function (a, b) { - return b.score - a.score; - }); - return results; - }; - - /** - * search queryTokens in specified field. - * - * @param {Array} queryTokens The query tokens to query in this field. - * @param {String} field Field to query in. - * @param {elasticlunr.Configuration} config The user query config, JSON format. - * @return {Object} - */ - elasticlunr.Index.prototype.fieldSearch = function ( - queryTokens, - fieldName, - config - ) { - const booleanType = config[fieldName].bool; - const expand = config[fieldName].expand; - const boost = config[fieldName].boost; - let scores = null; - const docTokens = {}; - - // Do nothing if the boost is 0 - if (boost === 0) { - return; - } - - queryTokens.forEach(function (token) { - let tokens = [token]; - if (expand === true) { - tokens = this.index[fieldName].expandToken(token); - } - // Consider every query token in turn. If expanded, each query token - // corresponds to a set of tokens, which is all tokens in the - // index matching the pattern queryToken* . - // For the set of tokens corresponding to a query token, find and score - // all matching documents. Store those scores in queryTokenScores, - // keyed by docRef. - // Then, depending on the value of booleanType, combine the scores - // for this query token with previous scores. If booleanType is OR, - // then merge the scores by summing into the accumulated total, adding - // new document scores are required (effectively a union operator). - // If booleanType is AND, accumulate scores only if the document - // has previously been scored by another query token (an intersection - // operation0. - // Furthermore, since when booleanType is AND, additional - // query tokens can't add new documents to the result set, use the - // current document set to limit the processing of each new query - // token for efficiency (i.e., incremental intersection). - - const queryTokenScores = {}; - tokens.forEach(function (key) { - let docs = this.index[fieldName].getDocs(key); - const idf = this.idf(key, fieldName); - - if (scores && booleanType === 'AND') { - // special case, we can rule out documents that have been - // already been filtered out because they weren't scored - // by previous query token passes. - const filteredDocs = {}; - for (var docRef in scores) { - if (docRef in docs) { - filteredDocs[docRef] = docs[docRef]; - } - } - docs = filteredDocs; - } - // only record appeared token for retrieved documents for the - // original token, not for expaned token. - // beause for doing coordNorm for a retrieved document, coordNorm only care how many - // query token appear in that document. - // so expanded token should not be added into docTokens, if added, this will pollute the - // coordNorm - if (key === token) { - this.fieldSearchStats(docTokens, key, docs); - } - - for (var docRef in docs) { - const tf = this.index[fieldName].getTermFrequency(key, docRef); - const fieldLength = this.documentStore.getFieldLength( - docRef, - fieldName - ); - let fieldLengthNorm = 1; - if (fieldLength !== 0) { - fieldLengthNorm = 1 / Math.sqrt(fieldLength); - } - - let penality = 1; - if (key !== token) { - // currently I'm not sure if this penality is enough, - // need to do verification - penality = - (1 - (key.length - token.length) / key.length) * 0.15; - } - - const score = tf * idf * fieldLengthNorm * penality; - - if (docRef in queryTokenScores) { - queryTokenScores[docRef] += score; - } else { - queryTokenScores[docRef] = score; - } - } - }, this); - - scores = this.mergeScores(scores, queryTokenScores, booleanType); - }, this); - - scores = this.coordNorm(scores, docTokens, queryTokens.length); - return scores; - }; - - /** - * Merge the scores from one set of tokens into an accumulated score table. - * Exact operation depends on the op parameter. If op is 'AND', then only the - * intersection of the two score lists is retained. Otherwise, the union of - * the two score lists is returned. For internal use only. - * - * @param {Object} bool accumulated scores. Should be null on first call. - * @param {String} scores new scores to merge into accumScores. - * @param {Object} op merge operation (should be 'AND' or 'OR'). - * - */ - - elasticlunr.Index.prototype.mergeScores = function (accumScores, scores, op) { - if (!accumScores) { - return scores; - } - if (op === 'AND') { - const intersection = {}; - for (var docRef in scores) { - if (docRef in accumScores) { - intersection[docRef] = accumScores[docRef] + scores[docRef]; - } - } - return intersection; - } else { - for (var docRef in scores) { - if (docRef in accumScores) { - accumScores[docRef] += scores[docRef]; - } else { - accumScores[docRef] = scores[docRef]; - } - } - return accumScores; - } - }; - - /** - * Record the occuring query token of retrieved doc specified by doc field. - * Only for inner user. - * - * @param {Object} docTokens a data structure stores which token appears in the retrieved doc. - * @param {String} token query token - * @param {Object} docs the retrieved documents of the query token - * - */ - elasticlunr.Index.prototype.fieldSearchStats = function (docTokens, token, docs) { - for (const doc in docs) { - if (doc in docTokens) { - docTokens[doc].push(token); - } else { - docTokens[doc] = [token]; - } - } - }; - - /** - * coord norm the score of a doc. - * if a doc contain more query tokens, then the score will larger than the doc - * contains less query tokens. - * - * only for inner use. - * - * @param {Object} results first results - * @param {Object} docs field search results of a token - * @param {Integer} n query token number - * @return {Object} - */ - elasticlunr.Index.prototype.coordNorm = function (scores, docTokens, n) { - for (const doc in scores) { - if (!(doc in docTokens)) continue; - const tokens = docTokens[doc].length; - scores[doc] = (scores[doc] * tokens) / n; - } - - return scores; - }; - - /** - * Returns a representation of the index ready for serialisation. - * - * @return {Object} - * @memberOf Index - */ - elasticlunr.Index.prototype.toJSON = function () { - const indexJson = {}; - this._fields.forEach(function (field) { - indexJson[field] = this.index[field].toJSON(); - }, this); - - return { - version: elasticlunr.version, - fields: this._fields, - ref: this._ref, - documentStore: this.documentStore.toJSON(), - index: indexJson, - 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 = elasticlunr(function () { - * this.use(myPlugin, 'arg1', 'arg2') - * }) - * - * @param {Function} plugin The plugin to apply. - * @memberOf Index - */ - elasticlunr.Index.prototype.use = function (plugin) { - const args = Array.prototype.slice.call(arguments, 1); - args.unshift(this); - plugin.apply(this, args); - }; - /*! - * elasticlunr.DocumentStore - * Copyright (C) 2017 Wei Song - */ - - /** - * elasticlunr.DocumentStore is a simple key-value document store used for storing sets of tokens for - * documents stored in index. - * - * elasticlunr.DocumentStore store original JSON format documents that you could build search snippet by this original JSON document. - * - * user could choose whether original JSON format document should be store, if no configuration then document will be stored defaultly. - * If user care more about the index size, user could select not store JSON documents, then this will has some defects, such as user - * could not use JSON document to generate snippets of search results. - * - * @param {Boolean} save If the original JSON document should be stored. - * @constructor - * @module - */ - elasticlunr.DocumentStore = function (save) { - if (save === null || save === undefined) { - this._save = true; - } else { - this._save = save; - } - - this.docs = {}; - this.docInfo = {}; - this.length = 0; - }; - - /** - * Loads a previously serialised document store - * - * @param {Object} serialisedData The serialised document store to load. - * @return {elasticlunr.DocumentStore} - */ - elasticlunr.DocumentStore.load = function (serialisedData) { - const store = new this(); - - store.length = serialisedData.length; - store.docs = serialisedData.docs; - store.docInfo = serialisedData.docInfo; - store._save = serialisedData.save; - - return store; - }; - - /** - * check if current instance store the original doc - * - * @return {Boolean} - */ - elasticlunr.DocumentStore.prototype.isDocStored = function () { - return this._save; - }; - - /** - * Stores the given doc in the document store against the given id. - * If docRef already exist, then update doc. - * - * Document is store by original JSON format, then you could use original document to generate search snippets. - * - * @param {Integer|String} docRef The key used to store the JSON format doc. - * @param {Object} doc The JSON format doc. - */ - elasticlunr.DocumentStore.prototype.addDoc = function (docRef, doc) { - if (!this.hasDoc(docRef)) this.length++; - - if (this._save === true) { - this.docs[docRef] = clone(doc); - } else { - this.docs[docRef] = null; - } - }; - - /** - * Retrieves the JSON doc from the document store for a given key. - * - * If docRef not found, return null. - * If user set not storing the documents, return null. - * - * @param {Integer|String} docRef The key to lookup and retrieve from the document store. - * @return {Object} - * @memberOf DocumentStore - */ - elasticlunr.DocumentStore.prototype.getDoc = function (docRef) { - if (this.hasDoc(docRef) === false) return null; - return this.docs[docRef]; - }; - - /** - * Checks whether the document store contains a key (docRef). - * - * @param {Integer|String} docRef The id to look up in the document store. - * @return {Boolean} - * @memberOf DocumentStore - */ - elasticlunr.DocumentStore.prototype.hasDoc = function (docRef) { - return docRef in this.docs; - }; - - /** - * Removes the value for a key in the document store. - * - * @param {Integer|String} docRef The id to remove from the document store. - * @memberOf DocumentStore - */ - elasticlunr.DocumentStore.prototype.removeDoc = function (docRef) { - if (!this.hasDoc(docRef)) return; - - delete this.docs[docRef]; - delete this.docInfo[docRef]; - this.length--; - }; - - /** - * Add field length of a document's field tokens from pipeline results. - * The field length of a document is used to do field length normalization even without the original JSON document stored. - * - * @param {Integer|String} docRef document's id or reference - * @param {String} fieldName field name - * @param {Integer} length field length - */ - elasticlunr.DocumentStore.prototype.addFieldLength = function ( - docRef, - fieldName, - length - ) { - if (docRef === null || docRef === undefined) return; - if (this.hasDoc(docRef) == false) return; - - if (!this.docInfo[docRef]) this.docInfo[docRef] = {}; - this.docInfo[docRef][fieldName] = length; - }; - - /** - * Update field length of a document's field tokens from pipeline results. - * The field length of a document is used to do field length normalization even without the original JSON document stored. - * - * @param {Integer|String} docRef document's id or reference - * @param {String} fieldName field name - * @param {Integer} length field length - */ - elasticlunr.DocumentStore.prototype.updateFieldLength = function ( - docRef, - fieldName, - length - ) { - if (docRef === null || docRef === undefined) return; - if (this.hasDoc(docRef) == false) return; - - this.addFieldLength(docRef, fieldName, length); - }; - - /** - * get field length of a document by docRef - * - * @param {Integer|String} docRef document id or reference - * @param {String} fieldName field name - * @return {Integer} field length - */ - elasticlunr.DocumentStore.prototype.getFieldLength = function (docRef, fieldName) { - if (docRef === null || docRef === undefined) return 0; - - if (!(docRef in this.docs)) return 0; - if (!(fieldName in this.docInfo[docRef])) return 0; - return this.docInfo[docRef][fieldName]; - }; - - /** - * Returns a JSON representation of the document store used for serialisation. - * - * @return {Object} JSON format - * @memberOf DocumentStore - */ - elasticlunr.DocumentStore.prototype.toJSON = function () { - return { - docs: this.docs, - docInfo: this.docInfo, - length: this.length, - save: this._save, - }; - }; - - /** - * Cloning object - * - * @param {Object} object in JSON format - * @return {Object} copied object - */ - function clone(obj) { - if (obj === null || typeof obj !== 'object') return obj; - - const copy = obj.constructor(); - - for (const attr in obj) { - if (obj.hasOwnProperty(attr)) copy[attr] = obj[attr]; - } - - return copy; - } - /*! - * elasticlunr.stemmer - * Copyright (C) 2017 Oliver Nightingale - * Copyright (C) 2017 Wei Song - * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt - */ - - /** - * elasticlunr.stemmer is an english language stemmer, this is a JavaScript - * implementation of the PorterStemmer taken from http://tartarus.org/~martin - * - * @module - * @param {String} str The string to stem - * @return {String} - * @see elasticlunr.Pipeline - */ - elasticlunr.stemmer = (function () { - const 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', - }; - - const step3list = { - icate: 'ic', - ative: '', - alize: 'al', - iciti: 'ic', - ical: 'ic', - ful: '', - ness: '', - }; - - const c = '[^aeiou]'; // consonant - const v = '[aeiouy]'; // vowel - const C = c + '[^aeiouy]*'; // consonant sequence - const V = v + '[aeiou]*'; // vowel sequence - - const mgr0 = '^(' + C + ')?' + V + C; // [C]VC... is m>0 - const meq1 = '^(' + C + ')?' + V + C + '(' + V + ')?$'; // [C]VC[V] is m=1 - const mgr1 = '^(' + C + ')?' + V + C + V + C; // [C]VCVC... is m>1 - const s_v = '^(' + C + ')?' + v; // vowel in stem - - const re_mgr0 = new RegExp(mgr0); - const re_mgr1 = new RegExp(mgr1); - const re_meq1 = new RegExp(meq1); - const re_s_v = new RegExp(s_v); - - const re_1a = /^(.+?)(ss|i)es$/; - const re2_1a = /^(.+?)([^s])s$/; - const re_1b = /^(.+?)eed$/; - const re2_1b = /^(.+?)(ed|ing)$/; - const re_1b_2 = /.$/; - const re2_1b_2 = /(at|bl|iz)$/; - const re3_1b_2 = new RegExp('([^aeiouylsz])\\1$'); - const re4_1b_2 = new RegExp('^' + C + v + '[^aeiouwxy]$'); - - const re_1c = /^(.+?[^aeiou])y$/; - const re_2 = - /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; - - const re_3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; - - const re_4 = - /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; - const re2_4 = /^(.+?)(s|t)(ion)$/; - - const re_5 = /^(.+?)e$/; - const re_5_1 = /ll$/; - const re3_5 = new RegExp('^' + C + v + '[^aeiouwxy]$'); - - const porterStemmer = function porterStemmer(w) { - let 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 = re_1a; - re2 = re2_1a; - - if (re.test(w)) { - w = w.replace(re, '$1$2'); - } else if (re2.test(w)) { - w = w.replace(re2, '$1$2'); - } - - // Step 1b - re = re_1b; - re2 = re2_1b; - if (re.test(w)) { - var fp = re.exec(w); - re = re_mgr0; - if (re.test(fp[1])) { - re = re_1b_2; - w = w.replace(re, ''); - } - } else if (re2.test(w)) { - var fp = re2.exec(w); - stem = fp[1]; - re2 = re_s_v; - if (re2.test(stem)) { - w = stem; - re2 = re2_1b_2; - re3 = re3_1b_2; - re4 = re4_1b_2; - if (re2.test(w)) { - w = w + 'e'; - } else if (re3.test(w)) { - re = re_1b_2; - w = w.replace(re, ''); - } else if (re4.test(w)) { - w = w + 'e'; - } - } - } - - // 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) - re = re_1c; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - w = stem + 'i'; - } - - // Step 2 - re = re_2; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - suffix = fp[2]; - re = re_mgr0; - if (re.test(stem)) { - w = stem + step2list[suffix]; - } - } - - // Step 3 - re = re_3; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - suffix = fp[2]; - re = re_mgr0; - if (re.test(stem)) { - w = stem + step3list[suffix]; - } - } - - // Step 4 - re = re_4; - re2 = re2_4; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = re_mgr1; - if (re.test(stem)) { - w = stem; - } - } else if (re2.test(w)) { - var fp = re2.exec(w); - stem = fp[1] + fp[2]; - re2 = re_mgr1; - if (re2.test(stem)) { - w = stem; - } - } - - // Step 5 - re = re_5; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = re_mgr1; - re2 = re_meq1; - re3 = re3_5; - if (re.test(stem) || (re2.test(stem) && !re3.test(stem))) { - w = stem; - } - } - - re = re_5_1; - re2 = re_mgr1; - if (re.test(w) && re2.test(w)) { - re = re_1b_2; - w = w.replace(re, ''); - } - - // and turn initial Y back to y - - if (firstch == 'y') { - w = firstch.toLowerCase() + w.substr(1); - } - - return w; - }; - - return porterStemmer; - })(); - - elasticlunr.Pipeline.registerFunction(elasticlunr.stemmer, 'stemmer'); - /*! - * elasticlunr.stopWordFilter - * Copyright (C) 2017 Oliver Nightingale - * Copyright (C) 2017 Wei Song - */ - - /** - * elasticlunr.stopWordFilter is an English language stop words filter, any words - * contained in the stop word 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. - * Currently this StopwordFilter using dictionary to do O(1) time complexity stop word filtering. - * - * @module - * @param {String} token The token to pass through the filter - * @return {String} - * @see elasticlunr.Pipeline - */ - elasticlunr.stopWordFilter = function (token) { - if (token && elasticlunr.stopWordFilter.stopWords[token] !== true) { - return token; - } - }; - - /** - * Remove predefined stop words - * if user want to use customized stop words, user could use this function to delete - * all predefined stopwords. - * - * @return {null} - */ - elasticlunr.clearStopWords = function () { - elasticlunr.stopWordFilter.stopWords = {}; - }; - - /** - * Add customized stop words - * user could use this function to add customized stop words - * - * @params {Array} words customized stop words - * @return {null} - */ - elasticlunr.addStopWords = function (words) { - if (words == null || Array.isArray(words) === false) return; - - words.forEach(function (word) { - elasticlunr.stopWordFilter.stopWords[word] = true; - }, this); - }; - - /** - * Reset to default stop words - * user could use this function to restore default stop words - * - * @return {null} - */ - elasticlunr.resetStopWords = function () { - elasticlunr.stopWordFilter.stopWords = elasticlunr.defaultStopWords; - }; - - elasticlunr.defaultStopWords = { - '': true, - a: true, - able: true, - about: true, - across: true, - after: true, - all: true, - almost: true, - also: true, - am: true, - among: true, - an: true, - and: true, - any: true, - are: true, - as: true, - at: true, - be: true, - because: true, - been: true, - but: true, - by: true, - can: true, - cannot: true, - could: true, - dear: true, - did: true, - do: true, - does: true, - either: true, - else: true, - ever: true, - every: true, - for: true, - from: true, - get: true, - got: true, - had: true, - has: true, - have: true, - he: true, - her: true, - hers: true, - him: true, - his: true, - how: true, - however: true, - i: true, - if: true, - in: true, - into: true, - is: true, - it: true, - its: true, - just: true, - least: true, - let: true, - like: true, - likely: true, - may: true, - me: true, - might: true, - most: true, - must: true, - my: true, - neither: true, - no: true, - nor: true, - not: true, - of: true, - off: true, - often: true, - on: true, - only: true, - or: true, - other: true, - our: true, - own: true, - rather: true, - said: true, - say: true, - says: true, - she: true, - should: true, - since: true, - so: true, - some: true, - than: true, - that: true, - the: true, - their: true, - them: true, - then: true, - there: true, - these: true, - they: true, - this: true, - tis: true, - to: true, - too: true, - twas: true, - us: true, - wants: true, - was: true, - we: true, - were: true, - what: true, - when: true, - where: true, - which: true, - while: true, - who: true, - whom: true, - why: true, - will: true, - with: true, - would: true, - yet: true, - you: true, - your: true, - }; - - elasticlunr.stopWordFilter.stopWords = elasticlunr.defaultStopWords; - - elasticlunr.Pipeline.registerFunction(elasticlunr.stopWordFilter, 'stopWordFilter'); - /*! - * elasticlunr.trimmer - * Copyright (C) 2017 Oliver Nightingale - * Copyright (C) 2017 Wei Song - */ - - /** - * elasticlunr.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 - * @return {String} - * @see elasticlunr.Pipeline - */ - elasticlunr.trimmer = function (token) { - if (token === null || token === undefined) { - throw new Error('token should not be undefined'); - } - - return token.replace(/^\W+/, '').replace(/\W+$/, ''); - }; - - elasticlunr.Pipeline.registerFunction(elasticlunr.trimmer, 'trimmer'); - /*! - * elasticlunr.InvertedIndex - * Copyright (C) 2017 Wei Song - * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt - */ - - /** - * elasticlunr.InvertedIndex is used for efficiently storing and - * lookup of documents that contain a given token. - * - * @constructor - */ - elasticlunr.InvertedIndex = function () { - this.root = { docs: {}, df: 0 }; - }; - - /** - * Loads a previously serialised inverted index. - * - * @param {Object} serialisedData The serialised inverted index to load. - * @return {elasticlunr.InvertedIndex} - */ - elasticlunr.InvertedIndex.load = function (serialisedData) { - const idx = new this(); - idx.root = serialisedData.root; - - return idx; - }; - - /** - * Adds a {token: tokenInfo} pair to the inverted index. - * If the token already exist, then update the tokenInfo. - * - * tokenInfo format: { ref: 1, tf: 2} - * tokenInfor should contains the document's ref and the tf(token frequency) of that token in - * the document. - * - * By default this function starts at the root of the current inverted index, however - * it can start at any node of the inverted index if required. - * - * @param {String} token - * @param {Object} tokenInfo format: { ref: 1, tf: 2} - * @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 elasticlunr.InvertedIndex - * is used. - * @memberOf InvertedIndex - */ - elasticlunr.InvertedIndex.prototype.addToken = function (token, tokenInfo, root) { - var root = root || this.root; - let idx = 0; - - while (idx <= token.length - 1) { - const key = token[idx]; - - if (!(key in root)) root[key] = { docs: {}, df: 0 }; - idx += 1; - root = root[key]; - } - - const docRef = tokenInfo.ref; - if (!root.docs[docRef]) { - // if this doc not exist, then add this doc - root.docs[docRef] = { tf: tokenInfo.tf }; - root.df += 1; - } else { - // if this doc already exist, then update tokenInfo - root.docs[docRef] = { tf: tokenInfo.tf }; - } - }; - - /** - * Checks whether a token is in this elasticlunr.InvertedIndex. - * - * - * @param {String} token The token to be checked - * @return {Boolean} - * @memberOf InvertedIndex - */ - elasticlunr.InvertedIndex.prototype.hasToken = function (token) { - if (!token) return false; - - let node = this.root; - - for (let i = 0; i < token.length; i++) { - if (!node[token[i]]) return false; - node = node[token[i]]; - } - - return true; - }; - - /** - * Retrieve a node from the inverted index for a given token. - * If token not found in this InvertedIndex, return null. - * - * - * @param {String} token The token to get the node for. - * @return {Object} - * @see InvertedIndex.prototype.get - * @memberOf InvertedIndex - */ - elasticlunr.InvertedIndex.prototype.getNode = function (token) { - if (!token) return null; - - let node = this.root; - - for (let i = 0; i < token.length; i++) { - if (!node[token[i]]) return null; - node = node[token[i]]; - } - - return node; - }; - - /** - * Retrieve the documents of a given token. - * If token not found, return {}. - * - * - * @param {String} token The token to get the documents for. - * @return {Object} - * @memberOf InvertedIndex - */ - elasticlunr.InvertedIndex.prototype.getDocs = function (token) { - const node = this.getNode(token); - if (node == null) { - return {}; - } - - return node.docs; - }; - - /** - * Retrieve term frequency of given token in given docRef. - * If token or docRef not found, return 0. - * - * - * @param {String} token The token to get the documents for. - * @param {String|Integer} docRef - * @return {Integer} - * @memberOf InvertedIndex - */ - elasticlunr.InvertedIndex.prototype.getTermFrequency = function (token, docRef) { - const node = this.getNode(token); - - if (node == null) { - return 0; - } - - if (!(docRef in node.docs)) { - return 0; - } - - return node.docs[docRef].tf; - }; - - /** - * Retrieve the document frequency of given token. - * If token not found, return 0. - * - * - * @param {String} token The token to get the documents for. - * @return {Object} - * @memberOf InvertedIndex - */ - elasticlunr.InvertedIndex.prototype.getDocFreq = function (token) { - const node = this.getNode(token); - - if (node == null) { - return 0; - } - - return node.df; - }; - - /** - * Remove the document identified by document's ref from the token in the inverted index. - * - * - * @param {String} token Remove the document from which token. - * @param {String} ref The ref of the document to remove from given token. - * @memberOf InvertedIndex - */ - elasticlunr.InvertedIndex.prototype.removeToken = function (token, ref) { - if (!token) return; - const node = this.getNode(token); - - if (node == null) return; - - if (ref in node.docs) { - delete node.docs[ref]; - node.df -= 1; - } - }; - - /** - * Find all the possible suffixes of given token using tokens currently in the inverted index. - * If token not found, return empty Array. - * - * @param {String} token The token to expand. - * @return {Array} - * @memberOf InvertedIndex - */ - elasticlunr.InvertedIndex.prototype.expandToken = function (token, memo, root) { - if (token == null || token == '') return []; - var memo = memo || []; - - if (root == void 0) { - root = this.getNode(token); - if (root == null) return memo; - } - - if (root.df > 0) memo.push(token); - - for (const key in root) { - if (key === 'docs') continue; - if (key === 'df') continue; - this.expandToken(token + key, memo, root[key]); - } - - return memo; - }; - - /** - * Returns a representation of the inverted index ready for serialisation. - * - * @return {Object} - * @memberOf InvertedIndex - */ - elasticlunr.InvertedIndex.prototype.toJSON = function () { - return { - root: this.root, - }; - }; - - /*! - * elasticlunr.Configuration - * Copyright (C) 2017 Wei Song - */ - - /** - * elasticlunr.Configuration is used to analyze the user search configuration. - * - * By elasticlunr.Configuration user could set query-time boosting, boolean model in each field. - * - * Currently configuration supports: - * 1. query-time boosting, user could set how to boost each field. - * 2. boolean model chosing, user could choose which boolean model to use for each field. - * 3. token expandation, user could set token expand to True to improve Recall. Default is False. - * - * Query time boosting must be configured by field category, "boolean" model could be configured - * by both field category or globally as the following example. Field configuration for "boolean" - * will overwrite global configuration. - * Token expand could be configured both by field category or golbally. Local field configuration will - * overwrite global configuration. - * - * configuration example: - * { - * fields:{ - * title: {boost: 2}, - * body: {boost: 1} - * }, - * bool: "OR" - * } - * - * "bool" field configuation overwrite global configuation example: - * { - * fields:{ - * title: {boost: 2, bool: "AND"}, - * body: {boost: 1} - * }, - * bool: "OR" - * } - * - * "expand" example: - * { - * fields:{ - * title: {boost: 2, bool: "AND"}, - * body: {boost: 1} - * }, - * bool: "OR", - * expand: true - * } - * - * "expand" example for field category: - * { - * fields:{ - * title: {boost: 2, bool: "AND", expand: true}, - * body: {boost: 1} - * }, - * bool: "OR" - * } - * - * setting the boost to 0 ignores the field (this will only search the title): - * { - * fields:{ - * title: {boost: 1}, - * body: {boost: 0} - * } - * } - * - * then, user could search with configuration to do query-time boosting. - * idx.search('oracle database', {fields: {title: {boost: 2}, body: {boost: 1}}}); - * - * - * @constructor - * - * @param {String} config user configuration - * @param {Array} fields fields of index instance - * @module - */ - elasticlunr.Configuration = function (config, fields) { - var config = config || ''; - - if (fields == undefined || fields == null) { - throw new Error('fields should not be null'); - } - - this.config = {}; - - let userConfig; - try { - userConfig = JSON.parse(config); - this.buildUserConfig(userConfig, fields); - } catch (error) { - elasticlunr.utils.warn( - 'user configuration parse failed, will use default configuration' - ); - this.buildDefaultConfig(fields); - } - }; - - /** - * Build default search configuration. - * - * @param {Array} fields fields of index instance - */ - elasticlunr.Configuration.prototype.buildDefaultConfig = function (fields) { - this.reset(); - fields.forEach(function (field) { - this.config[field] = { - boost: 1, - bool: 'OR', - expand: false, - }; - }, this); - }; - - /** - * Build user configuration. - * - * @param {JSON} config User JSON configuratoin - * @param {Array} fields fields of index instance - */ - elasticlunr.Configuration.prototype.buildUserConfig = function (config, fields) { - let global_bool = 'OR'; - let global_expand = false; - - this.reset(); - if ('bool' in config) { - global_bool = config.bool || global_bool; - } - - if ('expand' in config) { - global_expand = config.expand || global_expand; - } - - if ('fields' in config) { - for (const field in config.fields) { - if (fields.indexOf(field) > -1) { - const field_config = config.fields[field]; - let field_expand = global_expand; - if (field_config.expand != undefined) { - field_expand = field_config.expand; - } - - this.config[field] = { - boost: - field_config.boost || field_config.boost === 0 - ? field_config.boost - : 1, - bool: field_config.bool || global_bool, - expand: field_expand, - }; - } else { - elasticlunr.utils.warn( - 'field name in user configuration not found in index instance fields' - ); - } - } - } else { - this.addAllFields2UserConfig(global_bool, global_expand, fields); - } - }; - - /** - * Add all fields to user search configuration. - * - * @param {String} bool Boolean model - * @param {String} expand Expand model - * @param {Array} fields fields of index instance - */ - elasticlunr.Configuration.prototype.addAllFields2UserConfig = function ( - bool, - expand, - fields - ) { - fields.forEach(function (field) { - this.config[field] = { - boost: 1, - bool, - expand, - }; - }, this); - }; - - /** - * get current user configuration - */ - elasticlunr.Configuration.prototype.get = function () { - return this.config; - }; - - /** - * reset user search configuration. - */ - elasticlunr.Configuration.prototype.reset = function () { - this.config = {}; - }; - /** - * sorted_set.js is added only to make elasticlunr.js compatible with lunr-languages. - * if elasticlunr.js support different languages by default, this will make elasticlunr.js - * much bigger that not good for browser usage. - * - */ - - /*! - * lunr.SortedSet - * Copyright (C) 2017 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) { - const 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 () { - let i, element; - - for (i = 0; i < arguments.length; i++) { - element = arguments[i]; - if (~this.indexOf(element)) continue; - this.elements.splice(this.locationFor(element), 0, element); - } - - 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. - * @returns {Number} - * @memberOf SortedSet - */ - lunr.SortedSet.prototype.indexOf = function (elem) { - let start = 0; - let end = this.elements.length; - let sectionLength = end - start; - let pivot = start + Math.floor(sectionLength / 2); - let pivotElem = this.elements[pivot]; - - while (sectionLength > 1) { - if (pivotElem === elem) return pivot; - - if (pivotElem < elem) start = pivot; - if (pivotElem > elem) end = pivot; - - sectionLength = end - start; - pivot = start + Math.floor(sectionLength / 2); - pivotElem = this.elements[pivot]; - } - - if (pivotElem === elem) return pivot; - - return -1; - }; - - /** - * 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 - * @returns {Number} - * @memberOf SortedSet - */ - lunr.SortedSet.prototype.locationFor = function (elem) { - let start = 0; - let end = this.elements.length; - let sectionLength = end - start; - let pivot = start + Math.floor(sectionLength / 2); - let pivotElem = this.elements[pivot]; - - while (sectionLength > 1) { - if (pivotElem < elem) start = pivot; - if (pivotElem > elem) end = pivot; - - sectionLength = end - start; - pivot = start + Math.floor(sectionLength / 2); - pivotElem = this.elements[pivot]; - } - - if (pivotElem > elem) return pivot; - if (pivotElem < elem) return pivot + 1; - }; - - /** - * 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) { - const intersectSet = new lunr.SortedSet(); - let i = 0; - let j = 0; - const a_len = this.length; - const b_len = otherSet.length; - const a = this.elements; - const 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 () { - const 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) { - let longSet, shortSet, unionSet; - - if (this.length >= otherSet.length) { - (longSet = this), (shortSet = otherSet); - } else { - (longSet = otherSet), (shortSet = this); - } - - unionSet = longSet.clone(); - - for ( - let i = 0, shortSetElements = shortSet.toArray(); - i < shortSetElements.length; - i++ - ) { - unionSet.add(shortSetElements[i]); - } - - return unionSet; - }; - - /** - * Returns a representation of the sorted set ready for serialisation. - * - * @returns {Array} - * @memberOf SortedSet - */ - lunr.SortedSet.prototype.toJSON = function () { - return this.toArray(); - }; - /** - * export the module via AMD, CommonJS 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.elasticlunr = 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 elasticlunr; - }); -})(); - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // -// End of elasticlunr code (http://elasticlunr.com/elasticlunr.js) // -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - -window.onload = function () { - if (!document.body.contains(document.getElementById('searchModal'))) { - return; - } - - const lang = document.documentElement.lang; - const searchInput = document.getElementById('searchInput'); - const searchModal = document.getElementById('searchModal'); - const searchButton = document.getElementById('search-button'); - const clearSearchButton = document.getElementById('clear-search'); - const resultsContainer = document.getElementById('results-container'); - const results = document.getElementById('results'); - // Get all spans holding the translated strings, even if they are only used on one language. - const zeroResultsSpan = document.getElementById('zero_results'); - const oneResultsSpan = document.getElementById('one_results'); - const twoResultsSpan = document.getElementById('two_results'); - const fewResultsSpan = document.getElementById('few_results'); - const manyResultsSpan = document.getElementById('many_results'); - - // Static mapping of keys to spans. - const resultSpans = { - zero_results: zeroResultsSpan, - one_results: oneResultsSpan, - two_results: twoResultsSpan, - few_results: fewResultsSpan, - many_results: manyResultsSpan, - }; - - // Replace $SHORTCUT in search icon title with actual OS-specific shortcut. - function getShortcut() { - const userAgent = window.navigator.userAgent.toLowerCase(); - if (userAgent.includes('mac')) { - return 'Cmd + K'; - } else { - return 'Ctrl + K'; - } - } - - function setAttributes(element, attributeNames) { - const shortcut = getShortcut(); - attributeNames.forEach((attributeName) => { - let attributeValue = element.getAttribute(attributeName); - if (attributeValue) { - attributeValue = attributeValue.replace('$SHORTCUT', shortcut); - element.setAttribute(attributeName, attributeValue); - } - }); - } - setAttributes(searchButton, ['title', 'aria-label']); - - // Make search button keyboard accessible. - searchButton.addEventListener('keydown', function (event) { - if (event.key === 'Enter' || event.key === ' ') { - searchButton.click(); - } - }); - - let lastFocusedElement; - function openSearchModal() { - lastFocusedElement = document.activeElement; - loadSearchIndex(); - searchModal.style.display = 'block'; - searchInput.focus(); - } - - function closeModal() { - searchModal.style.display = 'none'; - clearSearch(); - if (lastFocusedElement && document.body.contains(lastFocusedElement)) { - lastFocusedElement.focus(); - } - } - - function toggleModalVisibility() { - const isModalOpen = searchModal.style.display === 'block'; - if (isModalOpen) { - closeModal(); - } else { - openSearchModal(); - } - } - - // Function to remove 'selected' class from all divs except the one passed. - function clearSelected(exceptDiv = null) { - const divs = results.querySelectorAll('#results > div'); - divs.forEach((div) => { - if (div !== exceptDiv) { - div.setAttribute('aria-selected', 'false'); - } - }); - } - - function updateSelection(div) { - if (div.getAttribute('aria-selected') !== 'true') { - clearSelected(div); - div.setAttribute('aria-selected', 'true'); - } - searchInput.setAttribute('aria-activedescendant', div.id); - } - - function clearSearch() { - searchInput.value = ''; - results.innerHTML = ''; - resultsContainer.style.display = 'none'; - searchInput.removeAttribute('aria-activedescendant'); - clearSearchButton.style.display = 'none'; - } - - // Close modal when clicking/tapping outside. - function handleModalInteraction(event) { - if (event.target === searchModal) { - closeModal(); - } - event.stopPropagation(); // Prevents tapping through the modal. - } - searchModal.addEventListener('click', handleModalInteraction); - searchModal.addEventListener('touchend', handleModalInteraction, { passive: true }); - - // Close modal when pressing escape. - document.addEventListener('keydown', function (event) { - if (event.key === 'Escape') { - closeModal(); - } - }); - - clearSearchButton.addEventListener('click', function () { - clearSearch(); - searchInput.focus(); - }); - clearSearchButton.addEventListener('keydown', function (event) { - if (event.key === 'Enter' || event.key === ' ') { - clearSearch(); - searchInput.focus(); - event.preventDefault(); - } - }); - - // The index loads on mouseover/tap. - // Clicking/tapping the search button opens the modal. - searchButton.addEventListener('mouseover', loadSearchIndex); - searchButton.addEventListener('click', openSearchModal); - searchButton.addEventListener('touchstart', openSearchModal, { passive: true }); - - let searchIndexPromise = null; - function loadSearchIndex() { - if (!searchIndexPromise) { - // Check if the search index is already loaded in the window object - if (window.searchIndex) { - // If the index is pre-loaded, use it directly. - searchIndexPromise = Promise.resolve( - elasticlunr.Index.load(window.searchIndex) - ); - } else { - // If the index is not pre-loaded, fetch it from the JSON file. - const language = document.documentElement - .getAttribute('lang') - .substring(0, 2); - let basePath = document - .querySelector("meta[name='base']") - .getAttribute('content'); - if (basePath.endsWith('/')) { - basePath = basePath.slice(0, -1); - } - - searchIndexPromise = fetch( - basePath + '/search_index.' + language + '.json' - ) - .then((response) => response.json()) - .then((json) => elasticlunr.Index.load(json)); - } - } - } - - function getByteByBinary(binaryCode) { - // Binary system, starts with `0b` in ES6 - // Octal number system, starts with `0` in ES5 and starts with `0o` in ES6 - // Hexadecimal, starts with `0x` in both ES5 and ES6 - var byteLengthDatas = [0, 1, 2, 3, 4]; - var len = byteLengthDatas[Math.ceil(binaryCode.length / 8)]; - return len; - } - - function getByteByHex(hexCode) { - return getByteByBinary(parseInt(hexCode, 16).toString(2)); - } - - function substringByByte(str, maxLength) { - let result = ''; - let flag = false; - let len = 0; - let length = 0; - let length2 = 0; - for (let i = 0; i < str.length; i++) { - const code = str.codePointAt(i).toString(16); - if (code.length > 4) { - i++; - if (i + 1 < str.length) { - flag = str.codePointAt(i + 1).toString(16) === '200d'; - } - } - if (flag) { - len += getByteByHex(code); - if (i == str.length - 1) { - length += len; - if (length <= maxLength) { - result += str.substr(length2, i - length2 + 1); - } else { - break; - } - } - } else { - if (len != 0) { - length += len; - length += getByteByHex(code); - if (length <= maxLength) { - result += str.substr(length2, i - length2 + 1); - length2 = i + 1; - } else { - break; - } - len = 0; - continue; - } - length += getByteByHex(code); - if (length <= maxLength) { - if (code.length <= 4) { - result += str[i]; - } else { - result += str[i - 1] + str[i]; - } - length2 = i + 1; - } else { - break; - } - } - } - return result; - } - - function generateSnippet(text, searchTerms) { - const BASE_SCORE = 2; - const FIRST_WORD_SCORE = 8; - const HIGHLIGHT_SCORE = 40; - const PRE_MATCH_CONTEXT_WORDS = 4; - const SNIPPET_LENGTH = 150; - const WINDOW_SIZE = 30; - - const stemmedTerms = searchTerms.map(function (term) { - return elasticlunr.stemmer(term.toLowerCase()); - }); - - let totalLength = 0; - const tokenScores = []; - const sentences = text.toLowerCase().split('. '); - - for (const sentence of sentences) { - const words = sentence.split(/[\s\n]/); - let isFirstWord = true; - - for (const word of words) { - if (word.length > 0) { - let score = isFirstWord ? FIRST_WORD_SCORE : BASE_SCORE; - for (const stemmedTerm of stemmedTerms) { - if (elasticlunr.stemmer(word).startsWith(stemmedTerm)) { - score = HIGHLIGHT_SCORE; - } - } - tokenScores.push([word, score, totalLength]); - isFirstWord = false; - } - totalLength += word.length + 1; - } - totalLength += 1; - } - - if (tokenScores.length === 0) { - return text.length > SNIPPET_LENGTH - ? text.substring(0, SNIPPET_LENGTH) + '…' - : text; - } - - const scores = []; - let windowScore = 0; - - for (var i = 0; i < Math.min(tokenScores.length, WINDOW_SIZE); i++) { - windowScore += tokenScores[i][1]; - } - scores.push(windowScore); - - // Slide the window and update the score. - for (var i = 1; i <= tokenScores.length - WINDOW_SIZE; i++) { - windowScore -= tokenScores[i - 1][1]; - windowScore += tokenScores[i + WINDOW_SIZE - 1][1]; - scores.push(windowScore); - } - - let maxScoreIndex = 0; - let maxScore = 0; - for (var i = scores.length - 1; i >= 0; i--) { - if (maxScore < scores[i]) { - maxScore = scores[i]; - maxScoreIndex = i; - } - } - - const snippet = []; - // From my testing, the context is more clear if we start a few words back. - let start = adjustStartPos( - text, - tokenScores[maxScoreIndex][2], - PRE_MATCH_CONTEXT_WORDS - ); - - function adjustStartPos(text, matchStartIndex, numWordsBack) { - let spaceCount = 0; - let index = matchStartIndex - 1; - while (index >= 0 && spaceCount < numWordsBack) { - if (text[index] === ' ' && text[index - 1] !== '.') { - spaceCount++; - } else if (text[index] === '.' && text[index + 1] === ' ') { - // Stop if the match is at the start of a sentence. - break; - } - index--; - } - return spaceCount === numWordsBack ? index + 1 : matchStartIndex; - } - const re = /^[\x00-\xff]+$/; // Regular expression for ASCII check. - for ( - var i = maxScoreIndex; - i < maxScoreIndex + WINDOW_SIZE && i < tokenScores.length; - i++ - ) { - const wordData = tokenScores[i]; - if (start < wordData[2]) { - snippet.push(text.substring(start, wordData[2])); - start = wordData[2]; - } - - if (wordData[1] === HIGHLIGHT_SCORE) { - snippet.push('<b>'); - } - const end = wordData[2] + wordData[0].length; - // Handle non-ASCII characters. - if (!re.test(wordData[0]) && wordData[0].length >= 12) { - const strBefore = text.substring(wordData[2], end); - const strAfter = substringByByte(strBefore, 12); - snippet.push(strAfter); - } else { - snippet.push(text.substring(wordData[2], end)); - } - - if (wordData[1] === HIGHLIGHT_SCORE) { - snippet.push('</b>'); - } - start = end; - } - - snippet.push('…'); - const joinedSnippet = snippet.join(''); - let truncatedSnippet = joinedSnippet; - if (joinedSnippet.replace(/<[^>]+>/g, '').length > SNIPPET_LENGTH) { - truncatedSnippet = joinedSnippet.substring(0, SNIPPET_LENGTH) + '…'; - } - - return truncatedSnippet; - } - - // Handle input in the search box. - searchInput.addEventListener( - 'input', - async function () { - const inputValue = this.value; - const searchTerm = inputValue.trim(); - const searchIndex = await searchIndexPromise; - results.innerHTML = ''; - - // Use the raw input so the "clear" button appears even if there's only spaces. - clearSearchButton.style.display = inputValue.length > 0 ? 'block' : 'none'; - resultsContainer.style.display = searchTerm.length > 0 ? 'block' : 'none'; - - // Perform the search and store the results. - const searchResults = searchIndex.search(searchTerm, { - bool: 'OR', - fields: { - title: { boost: 3 }, - body: { boost: 2 }, - description: { boost: 1 }, - path: { boost: 1 }, - }, - }); - - // Update the number of results. - updateResultText(searchResults.length); - - // Display the results. - let resultIdCounter = 0; // Counter to generate unique IDs. - searchResults.forEach(function (result) { - if (result.doc.title || result.doc.path || result.doc.id) { - const resultDiv = document.createElement('div'); - resultDiv.setAttribute('role', 'option'); - resultDiv.id = 'result-' + resultIdCounter++; - resultDiv.innerHTML = '<a href><span></span><span></span></a>'; - const linkElement = resultDiv.querySelector('a'); - const titleElement = resultDiv.querySelector('span:first-child'); - const snippetElement = resultDiv.querySelector('span:nth-child(2)'); - - // Determine the text for the title. - titleElement.textContent = - result.doc.title || result.doc.path || result.doc.id; - - // Determine if the body or description is available for the snippet. - let snippetText = result.doc.body - ? generateSnippet(result.doc.body, searchTerm.split(/\s+/)) - : result.doc.description - ? result.doc.description - : ''; - snippetElement.innerHTML = snippetText; - - // Create the hyperlink. - let href = result.ref; - if (result.doc.body) { - // Include text fragment if body is available. - const encodedSearchTerm = encodeURIComponent(searchTerm); - href += `#:~:text=${encodedSearchTerm}`; - } - linkElement.href = href; - - results.appendChild(resultDiv); - } - }); - - searchInput.setAttribute( - 'aria-expanded', - resultIdCounter > 0 ? 'true' : 'false' - ); - - if (results.firstChild) { - updateSelection(results.firstChild); - } - - results.addEventListener('mouseover', function (event) { - if (event.target.closest('div[role="option"]')) { - updateSelection(event.target.closest('div[role="option"]')); - } - }); - - results.addEventListener('click', function(event) { - const clickedElement = event.target.closest('a'); - if (clickedElement) { - const clickedHref = clickedElement.getAttribute('href'); - const currentPageUrl = window.location.href; - - // Normalise URLs by removing the text fragment and trailing slash. - const normalizeUrl = (url) => url.split('#')[0].replace(/\/$/, ''); - - // Check if the clicked link matches the current page. - // If using Ctrl+click or Cmd+click, don't close the modal. - if (normalizeUrl(clickedHref) === normalizeUrl(currentPageUrl) && - !event.ctrlKey && !event.metaKey) { - closeModal(); - } - } - }); - - // Add touch events to the results. - setupTouchEvents(); - }, - true - ); - - function updateResultText(count) { - // Determine the correct pluralization key based on count and language. - const pluralizationKey = getPluralizationKey(count, lang); - - // Hide all result text spans. - Object.values(resultSpans).forEach((span) => { - if (span) span.style.display = 'none'; - }); - - // Show the relevant result text span, replacing $NUMBER with the actual count. - const activeSpan = resultSpans[pluralizationKey]; - if (activeSpan) { - activeSpan.style.display = 'inline'; - activeSpan.textContent = activeSpan.textContent.replace( - '$NUMBER', - count.toString() - ); - } - } - - function getPluralizationKey(count, lang) { - let key = ''; - const slavicLangs = ['uk', 'be', 'bs', 'hr', 'ru', 'sr']; - - // Common cases: zero, one. - if (count === 0) { - key = 'zero_results'; - } else if (count === 1) { - key = 'one_results'; - } else { - // Arabic. - if (lang === 'ar') { - let modulo = count % 100; - if (count === 2) { - key = 'two_results'; - } else if (modulo >= 3 && modulo <= 10) { - key = 'few_results'; - } else { - key = 'many_results'; - } - } else if (slavicLangs.includes(lang)) { - // Slavic languages. - let modulo10 = count % 10; - let modulo100 = count % 100; - if (modulo10 === 1 && modulo100 !== 11) { - key = 'one_results'; - } else if ( - modulo10 >= 2 && - modulo10 <= 4 && - !(modulo100 >= 12 && modulo100 <= 14) - ) { - key = 'few_results'; - } else { - key = 'many_results'; - } - } else { - key = 'many_results'; // Default plural. - } - } - - return key; - } - - function setupTouchEvents() { - const resultDivs = document.querySelectorAll('#results > div'); - resultDivs.forEach((div) => { - // Remove existing listener to avoid duplicates. - div.removeEventListener('touchstart', handleTouchStart); - div.addEventListener('touchstart', handleTouchStart, { passive: true }); - }); - } - - function handleTouchStart() { - updateSelection(this); - } - - // Handle keyboard navigation. - document.addEventListener('keydown', function (event) { - // Add handling for the modal open/close shortcut. - const isMac = navigator.userAgent.toLowerCase().includes('mac'); - const MODAL_SHORTCUT_KEY = 'k'; - const modalShortcutModifier = isMac ? event.metaKey : event.ctrlKey; - - if (event.key === MODAL_SHORTCUT_KEY && modalShortcutModifier) { - event.preventDefault(); - toggleModalVisibility(); - return; - } - - const activeElement = document.activeElement; - if ( - event.key === 'Tab' && - (activeElement === searchInput || activeElement === clearSearchButton) - ) { - event.preventDefault(); - const nextFocusableElement = - activeElement === searchInput ? clearSearchButton : searchInput; - nextFocusableElement.focus(); - return; - } - - function updateResultSelection(newIndex, divsArray) { - updateSelection(divsArray[newIndex]); - divsArray[newIndex].scrollIntoView({ block: 'nearest', inline: 'start' }); - } - - const resultDivs = results.querySelectorAll('#results > div'); - if (resultDivs.length === 0) return; - - const divsArray = Array.from(resultDivs); - let activeDiv = results.querySelector('[aria-selected="true"]'); - let activeDivIndex = divsArray.indexOf(activeDiv); - - if ( - ['ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown'].includes( - event.key - ) - ) { - event.preventDefault(); - let newIndex = activeDivIndex; - - switch (event.key) { - case 'ArrowUp': - newIndex = Math.max(activeDivIndex - 1, 0); - break; - case 'ArrowDown': - newIndex = Math.min(activeDivIndex + 1, divsArray.length - 1); - break; - case 'Home': - newIndex = 0; - break; - case 'End': - newIndex = divsArray.length - 1; - break; - case 'PageUp': - newIndex = Math.max(activeDivIndex - 3, 0); - break; - case 'PageDown': - newIndex = Math.min(activeDivIndex + 3, divsArray.length - 1); - break; - } - - if (newIndex !== activeDivIndex) { - updateResultSelection(newIndex, divsArray); - } - } - - if (event.key === 'Enter' && activeDiv) { - event.preventDefault(); - event.stopImmediatePropagation(); - const anchorTag = activeDiv.querySelector('a'); - if (anchorTag) { - window.location.href = anchorTag.getAttribute('href'); - } - closeModal(); // Necessary when linking to the current page. - } - }); -}; |
