@wordpress/core-data
Version:
Access to and manipulation of core WordPress entities.
200 lines (195 loc) • 6.66 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = fetchLinkSuggestions;
exports.sortResults = sortResults;
exports.tokenize = tokenize;
var _apiFetch = _interopRequireDefault(require("@wordpress/api-fetch"));
var _url = require("@wordpress/url");
var _htmlEntities = require("@wordpress/html-entities");
var _i18n = require("@wordpress/i18n");
/**
* WordPress dependencies
*/
/**
* Fetches link suggestions from the WordPress API.
*
* WordPress does not support searching multiple tables at once, e.g. posts and terms, so we
* perform multiple queries at the same time and then merge the results together.
*
* @param search
* @param searchOptions
* @param editorSettings
*
* @example
* ```js
* import { __experimentalFetchLinkSuggestions as fetchLinkSuggestions } from '@wordpress/core-data';
*
* //...
*
* export function initialize( id, settings ) {
*
* settings.__experimentalFetchLinkSuggestions = (
* search,
* searchOptions
* ) => fetchLinkSuggestions( search, searchOptions, settings );
* ```
*/
async function fetchLinkSuggestions(search, searchOptions = {}, editorSettings = {}) {
const searchOptionsToUse = searchOptions.isInitialSuggestions && searchOptions.initialSuggestionsSearchOptions ? {
...searchOptions,
...searchOptions.initialSuggestionsSearchOptions
} : searchOptions;
const {
type,
subtype,
page,
perPage = searchOptions.isInitialSuggestions ? 3 : 20
} = searchOptionsToUse;
const {
disablePostFormats = false
} = editorSettings;
const queries = [];
if (!type || type === 'post') {
queries.push((0, _apiFetch.default)({
path: (0, _url.addQueryArgs)('/wp/v2/search', {
search,
page,
per_page: perPage,
type: 'post',
subtype
})
}).then(results => {
return results.map(result => {
return {
id: result.id,
url: result.url,
title: (0, _htmlEntities.decodeEntities)(result.title || '') || (0, _i18n.__)('(no title)'),
type: result.subtype || result.type,
kind: 'post-type'
};
});
}).catch(() => []) // Fail by returning no results.
);
}
if (!type || type === 'term') {
queries.push((0, _apiFetch.default)({
path: (0, _url.addQueryArgs)('/wp/v2/search', {
search,
page,
per_page: perPage,
type: 'term',
subtype
})
}).then(results => {
return results.map(result => {
return {
id: result.id,
url: result.url,
title: (0, _htmlEntities.decodeEntities)(result.title || '') || (0, _i18n.__)('(no title)'),
type: result.subtype || result.type,
kind: 'taxonomy'
};
});
}).catch(() => []) // Fail by returning no results.
);
}
if (!disablePostFormats && (!type || type === 'post-format')) {
queries.push((0, _apiFetch.default)({
path: (0, _url.addQueryArgs)('/wp/v2/search', {
search,
page,
per_page: perPage,
type: 'post-format',
subtype
})
}).then(results => {
return results.map(result => {
return {
id: result.id,
url: result.url,
title: (0, _htmlEntities.decodeEntities)(result.title || '') || (0, _i18n.__)('(no title)'),
type: result.subtype || result.type,
kind: 'taxonomy'
};
});
}).catch(() => []) // Fail by returning no results.
);
}
if (!type || type === 'attachment') {
queries.push((0, _apiFetch.default)({
path: (0, _url.addQueryArgs)('/wp/v2/media', {
search,
page,
per_page: perPage
})
}).then(results => {
return results.map(result => {
return {
id: result.id,
url: result.source_url,
title: (0, _htmlEntities.decodeEntities)(result.title.rendered || '') || (0, _i18n.__)('(no title)'),
type: result.type,
kind: 'media'
};
});
}).catch(() => []) // Fail by returning no results.
);
}
const responses = await Promise.all(queries);
let results = responses.flat();
results = results.filter(result => !!result.id);
results = sortResults(results, search);
results = results.slice(0, perPage);
return results;
}
/**
* Sort search results by relevance to the given query.
*
* Sorting is necessary as we're querying multiple endpoints and merging the results. For example
* a taxonomy title might be more relevant than a post title, but by default taxonomy results will
* be ordered after all the (potentially irrelevant) post results.
*
* We sort by scoring each result, where the score is the number of tokens in the title that are
* also in the search query, divided by the total number of tokens in the title. This gives us a
* score between 0 and 1, where 1 is a perfect match.
*
* @param results
* @param search
*/
function sortResults(results, search) {
const searchTokens = tokenize(search);
const scores = {};
for (const result of results) {
if (result.title) {
const titleTokens = tokenize(result.title);
const exactMatchingTokens = titleTokens.filter(titleToken => searchTokens.some(searchToken => titleToken === searchToken));
const subMatchingTokens = titleTokens.filter(titleToken => searchTokens.some(searchToken => titleToken !== searchToken && titleToken.includes(searchToken)));
// The score is a combination of exact matches and sub-matches.
// More weight is given to exact matches, as they are more relevant (e.g. "cat" vs "caterpillar").
// Diving by the total number of tokens in the title normalizes the score and skews
// the results towards shorter titles.
const exactMatchScore = exactMatchingTokens.length / titleTokens.length * 10;
const subMatchScore = subMatchingTokens.length / titleTokens.length;
scores[result.id] = exactMatchScore + subMatchScore;
} else {
scores[result.id] = 0;
}
}
return results.sort((a, b) => scores[b.id] - scores[a.id]);
}
/**
* Turns text into an array of tokens, with whitespace and punctuation removed.
*
* For example, `"I'm having a ball."` becomes `[ "im", "having", "a", "ball" ]`.
*
* @param text
*/
function tokenize(text) {
// \p{L} matches any kind of letter from any language.
// \p{N} matches any kind of numeric character.
return text.toLowerCase().match(/[\p{L}\p{N}]+/gu) || [];
}
//# sourceMappingURL=__experimental-fetch-link-suggestions.js.map