wikimedia/mediawiki-extensions-MobileFrontend

View on GitHub
src/mobile.startup/search/SearchGateway.js

Summary

Maintainability
A
0 mins
Test Coverage
/**
 * Internal for use inside Minerva only. See {@link module:mobile.startup} for access.
 *
 * @module module:mobile.startup/search
 */
const
    pageJSONParser = require( '../page/pageJSONParser' ),
    util = require( '../util' ),
    extendSearchParams = require( '../extendSearchParams' );

/**
 * Interact with MediaWiki search API.
 *
 * @memberof module:mobile.startup/search
 * @uses mw.Api
 * @param {mw.Api} api
 */
function SearchGateway( api ) {
    this.api = api;
    this.searchCache = {};
    this.generator = mw.config.get( 'wgMFSearchGenerator' );
}

SearchGateway.prototype = {
    /**
     * The namespace to search in.
     *
     * @memberof SearchGateway
     * @instance
     * @type {number}
     */
    searchNamespace: 0,

    /**
     * Get the data used to do the search query api call.
     *
     * @memberof SearchGateway
     * @instance
     * @param {string} query to search for
     * @return {Object}
     */
    getApiData( query ) {
        const prefix = this.generator.prefix,
            data = extendSearchParams( 'search', {
                generator: this.generator.name
            } );

        data.redirects = '';

        data['g' + prefix + 'search'] = query;
        data['g' + prefix + 'namespace'] = this.searchNamespace;
        data['g' + prefix + 'limit'] = 15;

        // If PageImages is being used configure further.
        if ( data.pilimit ) {
            data.pilimit = 15;
            data.pithumbsize = mw.config.get( 'wgMFThumbnailSizes' ).tiny;
        }
        return data;
    },

    /**
     * Escapes regular expression wildcards (metacharacters) by adding a \\ prefix
     *
     * @memberof SearchGateway
     * @instance
     * @param {string} str a string
     * @return {Object} a regular expression that can be used to search for that str
     * @private
     */
    _createSearchRegEx( str ) {
        // '\[' can be unescaped, but leave it balanced with '`]'
        // eslint-disable-next-line no-useless-escape
        str = str.replace( /[-\[\]{}()*+?.,\\^$|#\s]/g, '\\$&' );
        return new RegExp( '^(' + str + ')', 'ig' );
    },

    /**
     * Takes a label potentially beginning with term
     * and highlights term if it is present with strong
     *
     * @memberof SearchGateway
     * @instance
     * @param {string} label a piece of text
     * @param {string} term a string to search for from the start
     * @return {string} safe html string with matched terms encapsulated in strong tags
     * @private
     */
    _highlightSearchTerm( label, term ) {
        label = util.parseHTML( '<span>' ).text( label ).html();
        term = term.trim();
        term = util.parseHTML( '<span>' ).text( term ).html();

        return label.replace( this._createSearchRegEx( term ), '<strong>$1</strong>' );
    },

    /**
     * Return data used for creating {Page} objects
     *
     * @memberof SearchGateway
     * @instance
     * @param {string} query to search for
     * @param {Object} pageInfo from the API
     * @return {Object} data needed to create a {Page}
     * @private
     */
    _getPage( query, pageInfo ) {
        const page = pageJSONParser.parse( pageInfo );

        // If displaytext is set in the generator result (eg. by Wikibase),
        // use that as display title.
        // Otherwise default to the page's title.
        // FIXME: Given that displayTitle could have html in it be safe and just highlight text.
        // Note that highlightSearchTerm does full HTML escaping before highlighting.
        page.displayTitle = this._highlightSearchTerm(
            pageInfo.displaytext ? pageInfo.displaytext : page.title,
            query
        );
        page.index = pageInfo.index;

        return page;
    },

    /**
     * Process the data returned by the api call.
     *
     * @memberof SearchGateway
     * @instance
     * @param {string} query to search for
     * @param {Object} data from api
     * @return {Array}
     * @private
     */
    _processData( query, data ) {
        const self = this;

        let results = [];

        if ( data.query ) {

            results = data.query.pages || {};
            results = Object.keys( results ).map( ( id ) => self._getPage( query, results[id] ) );
            // sort in order of index
            results.sort( ( a, b ) => a.index - b.index );
        }

        return results;
    },

    /**
     * Perform a search for the given query.
     *
     * @memberof SearchGateway
     * @instance
     * @param {string} query to search for
     * @return {jQuery.Deferred}
     */
    search( query ) {
        const scriptPath = mw.config.get( 'wgMFScriptPath' ),
            self = this;

        if ( !this.isCached( query ) ) {
            const xhr = this.api.get( this.getApiData( query ), scriptPath ? {
                url: scriptPath
            } : undefined );
            const request = xhr
                .then( ( data, jqXHR ) => {
                    // resolve the Deferred object
                    return {
                        query,
                        results: self._processData( query, data ),
                        searchId: jqXHR && jqXHR.getResponseHeader( 'x-search-id' )
                    };
                }, () => {
                    // reset cached result, it maybe contains no value
                    self.searchCache[query] = undefined;
                } );

            // cache the result to prevent the execution of one search query twice
            // in one session
            this.searchCache[query] = request.promise( {
                abort() {
                    xhr.abort();
                }
            } );
        }

        return this.searchCache[query];
    },

    /**
     * Check if the search has already been performed in given session.
     *
     * @memberof SearchGateway
     * @instance
     * @param {string} query
     * @return {boolean}
     */
    isCached( query ) {
        return Boolean( this.searchCache[query] );
    }
};

module.exports = SearchGateway;