/**
 * 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.
        }
    });
};
