From 431599cb5904ab9cc124369ed1828660d89fb4a1 Mon Sep 17 00:00:00 2001 From: alistair3149 Date: Wed, 2 Aug 2023 18:56:08 -0400 Subject: [PATCH] =?UTF-8?q?refactor(search):=20=E2=99=BB=EF=B8=8F=20reimpl?= =?UTF-8?q?ement=20and=20clean=20up=20search=20clients?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This should make the search clients more readable and easier to expand --- .eslintrc.json | 1 + package.json | 1 + .../skins.citizen.search/gateway/gateway.js | 89 -------- .../gateway/mwActionApi.js | 119 ----------- .../skins.citizen.search/gateway/mwRestApi.js | 52 ----- .../searchClients/mwActionApi.js | 195 ++++++++++++++++++ .../searchClients/mwRestApi.js | 122 +++++++++++ .../searchClients/searchClients.json | 17 ++ .../{gateway => searchClients}/smwAskApi.js | 3 + resources/skins.citizen.search/typeahead.js | 50 +++-- resources/skins.citizen.search/types.js | 17 ++ .../skins.citizen.search/urlGenerator.js | 67 ++++++ skin.json | 9 +- 13 files changed, 461 insertions(+), 281 deletions(-) delete mode 100644 resources/skins.citizen.search/gateway/gateway.js delete mode 100644 resources/skins.citizen.search/gateway/mwActionApi.js delete mode 100644 resources/skins.citizen.search/gateway/mwRestApi.js create mode 100644 resources/skins.citizen.search/searchClients/mwActionApi.js create mode 100644 resources/skins.citizen.search/searchClients/mwRestApi.js create mode 100644 resources/skins.citizen.search/searchClients/searchClients.json rename resources/skins.citizen.search/{gateway => searchClients}/smwAskApi.js (97%) create mode 100644 resources/skins.citizen.search/types.js create mode 100644 resources/skins.citizen.search/urlGenerator.js diff --git a/.eslintrc.json b/.eslintrc.json index 5ed08e6f..1e1ee2d2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -19,6 +19,7 @@ "es-x/no-array-prototype-includes": "warn", "es-x/no-optional-chaining": "warn", "es-x/no-nullish-coalescing-operators": "warn", + "es-x/no-rest-spread-properties": "warn", "es-x/no-symbol-prototype-description": "warn", "compat/compat": "warn", "mediawiki/class-doc": "off" diff --git a/package.json b/package.json index 7001f86f..be08964b 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "devDependencies": { "@commitlint/cli": "^17.6.7", "@commitlint/config-conventional": "^17.6.7", + "@wikimedia/types-wikimedia": "0.4.1", "devmoji": "^2.3.0", "eslint-config-wikimedia": "0.25.1", "grunt-banana-checker": "0.11.0", diff --git a/resources/skins.citizen.search/gateway/gateway.js b/resources/skins.citizen.search/gateway/gateway.js deleted file mode 100644 index ec7c6313..00000000 --- a/resources/skins.citizen.search/gateway/gateway.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * @typedef {Object} Results - * @property {string} id The page ID of the page - * @property {string} title The title of the page. - * @property {string} desc The description of the page. - * @property {string} thumbnail The url of the thumbnail of the page. - * - * @global - */ - -const fetchJson = require( '../fetch.js' ); -const defaultGatewayName = require( '../config.json' ).wgCitizenSearchGateway; - -/** - * Setup the gateway based on the name provided - * - * @param {string} gatewayName - * @return {module} - */ -function getGateway( gatewayName ) { - switch ( gatewayName ) { - case 'mwActionApi': - return require( './mwActionApi.js' ); - case 'mwRestApi': - return require( './mwRestApi.js' ); - case 'smwAskApi': - return require( './smwAskApi.js' ); - default: - throw new Error( 'Unknown search gateway' ); - } -} - -/** - * Fetch suggestion from gateway and return the results object - * - * @param {string} searchQuery - * @return {Object} Results - */ -function getResults( searchQuery ) { - let gateway = getGateway( defaultGatewayName ); - - /* - * Multi-gateway search experiment - * This is a rough proof of concept for allowing multiple search gateway - * We are using SMW Ask as an experiment - * - * TODO: - * - Clean up gateway into JSON data perhaps - * - Implement UI support (initial states, search syntax suggestions) - * - Search query needs to be trimmed earlier so that query in the UI does not show the command - */ - const gatewayMap = new Map( [ - [ 'action', 'mwActionApi' ], - [ 'ask', 'smwAskApi' ], - [ 'rest', 'mwRestApi' ] - ] ); - - for ( const [ command, gatewayName ] of gatewayMap ) { - if ( searchQuery.startsWith( `/${command}` ) ) { - gateway = getGateway( gatewayName ); - /* Remove command (e.g. /smw) from query */ - searchQuery = searchQuery.slice( command.length + 1 ); - break; - } - } - - /* Abort early if there are no search query */ - if ( searchQuery.length === 0 ) { - return {}; - } - - const result = fetchJson( gateway.getUrl( searchQuery ), { - headers: { - accept: 'application/json' - } - } ); - const searchResponsePromise = result.fetch - .then( ( /** @type {RestResponse} */ res ) => { - return gateway.convertDataToResults( res ); - } ); - return { - abort: result.abort, - fetch: searchResponsePromise - }; -} - -module.exports = { - getResults: getResults -}; diff --git a/resources/skins.citizen.search/gateway/mwActionApi.js b/resources/skins.citizen.search/gateway/mwActionApi.js deleted file mode 100644 index ea1b9005..00000000 --- a/resources/skins.citizen.search/gateway/mwActionApi.js +++ /dev/null @@ -1,119 +0,0 @@ -const config = require( '../config.json' ), - descriptionSource = config.wgCitizenSearchDescriptionSource; - -/** - * Build URL used for fetch request - * - * @param {string} input - * @return {string} url - */ -function getUrl( input ) { - const endpoint = config.wgScriptPath + '/api.php?format=json', - cacheExpiry = config.wgSearchSuggestCacheExpiry, - maxResults = config.wgCitizenMaxSearchResults, - query = { - action: 'query', - smaxage: cacheExpiry, - maxage: cacheExpiry, - generator: 'prefixsearch', - prop: 'pageprops|pageimages', - redirects: '', - ppprop: 'displaytitle', - piprop: 'thumbnail', - pithumbsize: 200, - pilimit: maxResults, - gpssearch: input, - gpsnamespace: 0, - gpslimit: maxResults - }; - - switch ( descriptionSource ) { - case 'wikidata': - query.prop += '|description'; - break; - case 'textextracts': - query.prop += '|extracts'; - query.exchars = '60'; - query.exintro = '1'; - query.exlimit = maxResults; - query.explaintext = '1'; - break; - case 'pagedescription': - query.prop += '|pageprops'; - query.ppprop = 'description'; - break; - } - - let queryString = ''; - for ( const property in query ) { - queryString += '&' + property + '=' + query[ property ]; - } - - return endpoint + queryString; -} - -/** - * Map raw response to Results object - * - * @param {Object} data - * @return {Object} Results - */ -function convertDataToResults( data ) { - const getDisplayTitle = ( item ) => { - if ( item.pageprops && item.pageprops.displaytitle ) { - return item.pageprops.displaytitle; - } else { - return item.title; - } - }; - - const getDescription = ( item ) => { - switch ( descriptionSource ) { - case 'wikidata': - /* eslint-disable-next-line es-x/no-symbol-prototype-description */ - return item.description || ''; - case 'textextracts': - return item.extract || ''; - case 'pagedescription': - /* eslint-disable es-x/no-symbol-prototype-description */ - if ( item.pageprops && item.pageprops.description ) { - return item.pageprops.description.slice( 0, 60 ) + '...'; - /* eslint-enable es-x/no-symbol-prototype-description */ - } else { - return ''; - } - } - }; - - const results = []; - - if ( typeof ( ( data || [] ).query || [] ).pages === 'undefined' ) { - return []; - } - - data = Object.values( data.query.pages ); - - // Sort the data with the index property since it is not in order - data.sort( ( a, b ) => { - return a.index - b.index; - } ); - - for ( let i = 0; i < data.length; i++ ) { - results[ i ] = { - id: data[ i ].pageid, - key: data[ i ].title, - title: getDisplayTitle( data[ i ] ), - desc: getDescription( data[ i ] ) - }; - if ( data[ i ].thumbnail && data[ i ].thumbnail.source ) { - results[ i ].thumbnail = data[ i ].thumbnail.source; - } - } - - return results; -} - -module.exports = { - getUrl: getUrl, - convertDataToResults: convertDataToResults -}; diff --git a/resources/skins.citizen.search/gateway/mwRestApi.js b/resources/skins.citizen.search/gateway/mwRestApi.js deleted file mode 100644 index a8c78ac4..00000000 --- a/resources/skins.citizen.search/gateway/mwRestApi.js +++ /dev/null @@ -1,52 +0,0 @@ -const config = require( '../config.json' ); - -/** - * Build URL used for fetch request - * - * @param {string} input - * @return {string} url - */ -function getUrl( input ) { - const endpoint = config.wgScriptPath + '/rest.php/v1/search/title?q=', - query = '&limit=' + config.wgCitizenMaxSearchResults; - - return endpoint + input + query; -} - -/** - * Map raw response to Results object - * - * @param {Object} data - * @return {Object} Results - */ -function convertDataToResults( data ) { - const results = []; - - data = ( data || [] ).pages || []; - - for ( let i = 0; i < data.length; i++ ) { - results[ i ] = { - id: data[ i ].id, - key: data[ i ].key, - title: data[ i ].title, - /* eslint-disable-next-line es-x/no-symbol-prototype-description */ - desc: data[ i ].description - }; - // Redirect title - // Since 1.38 - if ( data[ i ].matched_title ) { - results[ i ].matchedTitle = data[ i ].matched_title; - } - - if ( data[ i ].thumbnail && data[ i ].thumbnail.url ) { - results[ i ].thumbnail = data[ i ].thumbnail.url; - } - } - - return results; -} - -module.exports = { - getUrl: getUrl, - convertDataToResults: convertDataToResults -}; diff --git a/resources/skins.citizen.search/searchClients/mwActionApi.js b/resources/skins.citizen.search/searchClients/mwActionApi.js new file mode 100644 index 00000000..65dfa19d --- /dev/null +++ b/resources/skins.citizen.search/searchClients/mwActionApi.js @@ -0,0 +1,195 @@ +/** @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, 60 ); + } + }; + + response = response.query; + + // Merge redirects array into pages array if avaliable + // So the from key can be used for matched title + if ( response.redirects ) { + response.pages = Object.values( + [ ...response.redirects, ...response.pages ].reduce( ( acc, curr ) => { + const index = curr.index; + acc[ index ] = { ...acc[ index ], ...curr }; + return acc; + }, [] ) + ); + } + + // 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, + 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', + prop: 'pageprops|pageimages', + redirects: '', + ppprop: 'displaytitle', + pilicense: 'any', + piprop: 'thumbnail', + pithumbsize: 200, + pilimit: limit.toString(), + gpssearch: q, + gpslimit: limit.toString() + }; + switch ( descriptionSource ) { + case 'wikidata': + params.prop += '|description'; + params.descprefersource = 'local'; + break; + case 'textextracts': + params.prop += '|extracts'; + params.exchars = '60'; + 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 ) => { + return adaptApiResponse( config, q, res, showDescription ); + } ); + return { + abort: result.abort, + fetch: searchResponsePromise + }; + } + }; +} + +module.exports = mwActionApiSearchClient; diff --git a/resources/skins.citizen.search/searchClients/mwRestApi.js b/resources/skins.citizen.search/searchClients/mwRestApi.js new file mode 100644 index 00000000..a64a3516 --- /dev/null +++ b/resources/skins.citizen.search/searchClients/mwRestApi.js @@ -0,0 +1,122 @@ +/** @module mwRestApiSearchClient */ + +const fetchJson = require( '../fetch.js' ); +const urlGenerator = require( '../urlGenerator.js' ); + +/** + * @typedef {Object} RestResponse + * @property {RestResult[]} pages + */ + +/** + * @typedef {Object} RestResult + * @property {number} id + * @property {string} key + * @property {string} title + * @property {string | null } matched_title + * @property {string} [description] + * @property {RestThumbnail | null} [thumbnail] + */ + +/** + * @typedef {Object} RestThumbnail + * @property {string} url + * @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 ); + return { + query, + results: response.pages.map( ( page ) => { + const thumbnail = page.thumbnail; + return { + id: page.id, + label: page.matched_title, + key: page.key, + title: page.title, + description: showDescription ? page.description : undefined, + url: urlGeneratorInstance.generateUrl( page ), + thumbnail: thumbnail ? { + url: thumbnail.url, + 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 mwRestApiSearchClient( config ) { + return { + /** + * @type {fetchByTitle} + */ + fetchByTitle: ( q, limit = config.wgCitizenMaxSearchResults, showDescription = true ) => { + const searchApiUrl = config.wgScriptPath + '/rest.php'; + const params = { q, limit: limit.toString() }; + const search = new URLSearchParams( params ); + const url = `${searchApiUrl}/v1/search/title?${search.toString()}`; + const result = fetchJson( url, { + headers: { + accept: 'application/json' + } + } ); + const searchResponsePromise = result.fetch + .then( ( /** @type {RestResponse} */ res ) => { + return adaptApiResponse( config, q, res, showDescription ); + } ); + return { + abort: result.abort, + fetch: searchResponsePromise + }; + } + }; +} + +module.exports = mwRestApiSearchClient; diff --git a/resources/skins.citizen.search/searchClients/searchClients.json b/resources/skins.citizen.search/searchClients/searchClients.json new file mode 100644 index 00000000..266c4076 --- /dev/null +++ b/resources/skins.citizen.search/searchClients/searchClients.json @@ -0,0 +1,17 @@ +[ + { + "id": "mwActionApi", + "name": "MediaWiki search", + "command": "action" + }, + { + "id": "mwRestApi", + "name": "MediaWiki search", + "command": "rest" + }, + { + "id": "smwAskApi", + "name": "Semantic search", + "command": "ask" + } +] diff --git a/resources/skins.citizen.search/gateway/smwAskApi.js b/resources/skins.citizen.search/searchClients/smwAskApi.js similarity index 97% rename from resources/skins.citizen.search/gateway/smwAskApi.js rename to resources/skins.citizen.search/searchClients/smwAskApi.js index 4d8aff2b..f39c95ed 100644 --- a/resources/skins.citizen.search/gateway/smwAskApi.js +++ b/resources/skins.citizen.search/searchClients/smwAskApi.js @@ -1,3 +1,6 @@ +/* + * TODO: This is going to be refactored soon as it is using the old standard +*/ const config = require( '../config.json' ); /** diff --git a/resources/skins.citizen.search/typeahead.js b/resources/skins.citizen.search/typeahead.js index 09cfcc83..6cd8dff7 100644 --- a/resources/skins.citizen.search/typeahead.js +++ b/resources/skins.citizen.search/typeahead.js @@ -5,11 +5,27 @@ const ACTIVE_CLASS = `${ITEM_CLASS}--active`, HIDDEN_CLASS = `${ITEM_CLASS}--hidden`; -/** - * Config object from getCitizenSearchResourceLoaderConfig() - */ +// Config object from getCitizenSearchResourceLoaderConfig() const config = require( './config.json' ); -const gateway = require( './gateway/gateway.js' ); + +// Search clients definition +const searchClients = require( './searchClients/searchClients.json' ); + +const searchClient = { + active: null, + getData: function ( id ) { + const data = Object.values( searchClients ).find( ( item ) => item.id === id ); + return data; + }, + setActive: function ( id ) { + const data = this.getData( id ); + if ( data ) { + const client = require( `./searchClients/${data.id}.js` ); + this.active = data; + this.active.client = client( config ); + } + } +}; const activeIndex = { index: -1, @@ -165,9 +181,7 @@ function clearSuggestions() { function getSuggestions( searchQuery, htmlSafeSearchQuery, placeholder ) { const renderSuggestions = ( results ) => { if ( results.length > 0 ) { - const - fragment = document.createDocumentFragment(), - suggestionLinkPrefix = `${config.wgScriptPath}/index.php?title=Special:Search&search=`; + const fragment = document.createDocumentFragment(); /** * Return the redirect title with search query highlight * @@ -225,16 +239,15 @@ function getSuggestions( searchQuery, htmlSafeSearchQuery, placeholder ) { id: `${PREFIX}-suggestion-${index}`, type: 'page', size: 'md', - link: suggestionLinkPrefix + encodeURIComponent( result.key ), + link: result.url, title: highlightTitle( result.title ), - // Just to be safe, not sure if the default API is HTML escaped - desc: result.desc + desc: result.description }; - if ( result.matchedTitle ) { - data.label = getRedirectLabel( result.title, result.matchedTitle ); + if ( result.label ) { + data.label = getRedirectLabel( result.title, result.label ); } if ( result.thumbnail ) { - data.thumbnail = result.thumbnail; + data.thumbnail = result.thumbnail.url; } else { // Thumbnail placeholder icon data.icon = 'image'; @@ -263,17 +276,17 @@ function getSuggestions( searchQuery, htmlSafeSearchQuery, placeholder ) { // Add loading animation searchInput.parentNode.classList.add( SEARCH_LOADING_CLASS ); - const { abort, fetch } = gateway.getResults( searchQuery ); + const { abort, fetch } = searchClient.active.client.fetchByTitle( searchQuery ); // Abort fetch if the input is detected // So that fetch request won't be queued up searchInput.addEventListener( 'input', abort, { once: true } ); - fetch?.then( ( results ) => { + fetch?.then( ( response ) => { searchInput.removeEventListener( 'input', abort ); clearSuggestions(); - if ( results !== null ) { - renderSuggestions( results ); + if ( response.results !== null ) { + renderSuggestions( response.results ); updateActiveIndex(); } } ).catch( ( error ) => { @@ -498,6 +511,9 @@ function initTypeahead( searchForm, input ) { // Init the value in case of undef error updateActiveIndex(); + // Set default active search client + searchClient.setActive( config.wgCitizenSearchGateway ); + // Since searchInput is focused before the event listener is set up onFocus(); searchInput.addEventListener( 'focus', onFocus ); diff --git a/resources/skins.citizen.search/types.js b/resources/skins.citizen.search/types.js new file mode 100644 index 00000000..2d2f2b61 --- /dev/null +++ b/resources/skins.citizen.search/types.js @@ -0,0 +1,17 @@ +/** + * @typedef {Object} SearchResult + * @property {number} id + * @property {string} key + * @property {string} title + * @property {string} [description] + * @property {SearchResultThumbnail} [thumbnail] + */ + +/** + * @typedef {Object} SearchResultThumbnail + * @property {string} url + * @property {number} [width] + * @property {number} [height] + */ + +/* exported SearchResult */ diff --git a/resources/skins.citizen.search/urlGenerator.js b/resources/skins.citizen.search/urlGenerator.js new file mode 100644 index 00000000..7cf214eb --- /dev/null +++ b/resources/skins.citizen.search/urlGenerator.js @@ -0,0 +1,67 @@ +/* + * Adapted from Vector + * All credits go to the developers behind Vector + * @see https://github.com/wikimedia/mediawiki-skins-Vector/blob/master/resources/skins.vector.search/urlGenerator.js +*/ + +/** + * @typedef {Record} UrlParams + * @param {string} title + * @param {string} fulltext + */ + +/** + * @callback generateUrl + * @param {SearchResult|string} searchResult + * @param {UrlParams} [params] + * @param {string} [articlePath] + * @return {string} + */ + +/** + * @typedef {Object} UrlGenerator + * @property {generateUrl} generateUrl + */ + +/** + * Generates URLs for suggestions like those in MediaWiki's mediawiki.searchSuggest implementation. + * + * @param {MwMap} config + * @return {UrlGenerator} + */ +function urlGenerator( config ) { + return { + /** + * @param {SearchResult|string} suggestion + * @param {UrlParams} params + * @param {string} articlePath + * @return {string} + */ + generateUrl( + suggestion, + params = { + title: 'Special:Search' + }, + articlePath = config.wgScript + ) { + if ( typeof suggestion !== 'string' ) { + suggestion = suggestion.title; + } else { + // Add `fulltext` query param to search within pages and for navigation + // to the search results page (prevents being redirected to a certain + // article). + params = Object.assign( {}, params, { + fulltext: '1' + } ); + } + + const searchParams = new URLSearchParams( + Object.assign( {}, params, { search: suggestion } ) + ); + return `${articlePath}?${searchParams.toString()}`; + } + }; +} + +/** @module urlGenerator */ +module.exports = urlGenerator; diff --git a/skin.json b/skin.json index 594efd03..fba52219 100644 --- a/skin.json +++ b/skin.json @@ -143,6 +143,7 @@ ] }, "skins.citizen.search": { + "es6": true, "templates": [ "resources/skins.citizen.search/templates/typeahead.mustache" ], @@ -157,10 +158,10 @@ }, "resources/skins.citizen.search/typeahead.js", "resources/skins.citizen.search/fetch.js", - "resources/skins.citizen.search/gateway/gateway.js", - "resources/skins.citizen.search/gateway/mwActionApi.js", - "resources/skins.citizen.search/gateway/mwRestApi.js", - "resources/skins.citizen.search/gateway/smwAskApi.js" + "resources/skins.citizen.search/urlGenerator.js", + "resources/skins.citizen.search/searchClients/searchClients.json", + "resources/skins.citizen.search/searchClients/mwActionApi.js", + "resources/skins.citizen.search/searchClients/mwRestApi.js" ], "messages": [ "citizen-search-fulltext",