/** @module mwActionApiSearchClient */ const fetchJson = require( '../fetch.js' ); const urlGenerator = require( '../urlGenerator.js' ); /** * @typedef {Object} ActionResponse * @property {ActionQuery[]} query */ /** * @typedef {Object} ActionQuery * @property {ActionRedirects[] | null} redirects * @property {ActionResult[]} pages */ /** * @typedef {Object} ActionRedirects * @property {string} from */ /** * @typedef {Object} ActionResult * @property {number} pageid * @property {number} index * @property {string} title * @property {ActionThumbnail | null} [thumbnail] */ /** * @typedef {Object} ActionThumbnail * @property {string} source * @property {number | null} [width] * @property {number | null} [height] */ /** * @typedef {Object} SearchResponse * @property {string} query * @property {SearchResult[]} results */ /** * @param {MwMap} config * @param {string} query * @param {Object} response * @param {boolean} showDescription * @return {SearchResponse} */ function adaptApiResponse( config, query, response, showDescription ) { const urlGeneratorInstance = urlGenerator( config ); const getDescription = ( page ) => { switch ( config.wgCitizenSearchDescriptionSource ) { case 'wikidata': return page?.description; case 'textextracts': return page?.extract; case 'pagedescription': return page?.pageprops?.description?.slice( 0, 100 ); } }; // Early exit with there are no query if ( !response.query ) { return { query, results: {} }; } response = response.query; // Merge redirects array into pages array if avaliable // So the from key can be used for matched title if ( response.redirects ) { const pageCount = response.pages.length; response.pages = Object.values( [ ...response.redirects, ...response.pages ].reduce( ( acc, curr ) => { const index = curr.index; acc[ index ] = { ...acc[ index ], ...curr }; return acc; }, [] ) ); // Sometimes there can be multiple redirect object for the same page, only take the one with lower index if ( response.pages.length !== pageCount ) { response.pages = response.pages.filter( ( obj ) => Object.prototype.hasOwnProperty.call( obj, 'title' ) ); } } // Sort pages by index key instead of page id response.pages.sort( ( a, b ) => a.index - b.index ); return { query, results: response.pages.map( ( page ) => { const thumbnail = page.thumbnail; return { id: page.pageid, label: page.from || page.title, key: page.title.replace( / /g, '_' ), title: page.title, description: showDescription ? getDescription( page ) : undefined, url: urlGeneratorInstance.generateUrl( page ), thumbnail: thumbnail ? { url: thumbnail.source, width: thumbnail.width ?? undefined, height: thumbnail.height ?? undefined } : undefined }; } ) }; } /** * @typedef {Object} AbortableSearchFetch * @property {Promise} fetch * @property {Function} abort */ /** * @callback fetchByTitle * @param {string} query The search term. * @param {number} [limit] Maximum number of results. * @return {AbortableSearchFetch} */ /** * @callback loadMore * @param {string} query The search term. * @param {number} offset The number of search results that were already loaded. * @param {number} [limit] How many further search results to load (at most). * @return {AbortableSearchFetch} */ /** * @typedef {Object} SearchClient * @property {fetchByTitle} fetchByTitle * @property {loadMore} [loadMore] */ /** * @param {MwMap} config * @return {SearchClient} */ function mwActionApiSearchClient( config ) { return { /** * @type {fetchByTitle} */ fetchByTitle: ( q, limit = config.wgCitizenMaxSearchResults, showDescription = true ) => { const cacheExpiry = config.wgSearchSuggestCacheExpiry; const descriptionSource = config.wgCitizenSearchDescriptionSource; const searchApiUrl = config.wgScriptPath + '/api.php'; const params = { format: 'json', formatversion: '2', action: 'query', smaxage: cacheExpiry, maxage: cacheExpiry, generator: 'prefixsearch', gpssearch: q, gpslimit: limit.toString(), redirects: '', prop: 'pageprops|pageimages', ppprop: 'displaytitle', piprop: 'thumbnail', pilicense: 'any', pithumbsize: 200, pilimit: limit.toString() }; switch ( descriptionSource ) { case 'wikidata': params.prop += '|description'; params.descprefersource = 'local'; break; case 'textextracts': params.prop += '|extracts'; params.exchars = '100'; params.exintro = '1'; params.exlimit = limit.toString(); params.explaintext = '1'; break; case 'pagedescription': params.prop += '|pageprops'; params.ppprop = 'description'; break; } const search = new URLSearchParams( params ); const url = `${ searchApiUrl }?${ search.toString() }`; const result = fetchJson( url, { headers: { accept: 'application/json' } } ); const searchResponsePromise = result.fetch .then( ( /** @type {ActionResponse} */ res ) => adaptApiResponse( config, q, res, showDescription ) ); return { abort: result.abort, fetch: searchResponsePromise }; } }; } module.exports = mwActionApiSearchClient;