UNPKG

@wordpress/block-editor

Version:
214 lines (184 loc) 5.76 kB
/** * External dependencies */ import removeAccents from 'remove-accents'; import { noCase } from 'change-case'; // Default search helpers. const defaultGetName = ( item ) => item.name || ''; const defaultGetTitle = ( item ) => item.title; const defaultGetDescription = ( item ) => item.description || ''; const defaultGetKeywords = ( item ) => item.keywords || []; const defaultGetCategory = ( item ) => item.category; const defaultGetCollection = () => null; // Normalization regexes const splitRegexp = [ /([\p{Ll}\p{Lo}\p{N}])([\p{Lu}\p{Lt}])/gu, // One lowercase or digit, followed by one uppercase. /([\p{Lu}\p{Lt}])([\p{Lu}\p{Lt}][\p{Ll}\p{Lo}])/gu, // One uppercase followed by one uppercase and one lowercase. ]; const stripRegexp = /(\p{C}|\p{P}|\p{S})+/giu; // Anything that's not a punctuation, symbol or control/format character. // Normalization cache const extractedWords = new Map(); const normalizedStrings = new Map(); /** * Extracts words from an input string. * * @param {string} input The input string. * * @return {Array} Words, extracted from the input string. */ export function extractWords( input = '' ) { if ( extractedWords.has( input ) ) { return extractedWords.get( input ); } const result = noCase( input, { splitRegexp, stripRegexp, } ) .split( ' ' ) .filter( Boolean ); extractedWords.set( input, result ); return result; } /** * Sanitizes the search input string. * * @param {string} input The search input to normalize. * * @return {string} The normalized search input. */ export function normalizeString( input = '' ) { if ( normalizedStrings.has( input ) ) { return normalizedStrings.get( input ); } // Disregard diacritics. // Input: "média" let result = removeAccents( input ); // Accommodate leading slash, matching autocomplete expectations. // Input: "/media" result = result.replace( /^\//, '' ); // Lowercase. // Input: "MEDIA" result = result.toLowerCase(); normalizedStrings.set( input, result ); return result; } /** * Converts the search term into a list of normalized terms. * * @param {string} input The search term to normalize. * * @return {string[]} The normalized list of search terms. */ export const getNormalizedSearchTerms = ( input = '' ) => { return extractWords( normalizeString( input ) ); }; const removeMatchingTerms = ( unmatchedTerms, unprocessedTerms ) => { return unmatchedTerms.filter( ( term ) => ! getNormalizedSearchTerms( unprocessedTerms ).some( ( unprocessedTerm ) => unprocessedTerm.includes( term ) ) ); }; export const searchBlockItems = ( items, categories, collections, searchInput ) => { const normalizedSearchTerms = getNormalizedSearchTerms( searchInput ); if ( normalizedSearchTerms.length === 0 ) { return items; } const config = { getCategory: ( item ) => categories.find( ( { slug } ) => slug === item.category )?.title, getCollection: ( item ) => collections[ item.name.split( '/' )[ 0 ] ]?.title, }; return searchItems( items, searchInput, config ); }; /** * Filters an item list given a search term. * * @param {Array} items Item list * @param {string} searchInput Search input. * @param {Object} config Search Config. * * @return {Array} Filtered item list. */ export const searchItems = ( items = [], searchInput = '', config = {} ) => { const normalizedSearchTerms = getNormalizedSearchTerms( searchInput ); if ( normalizedSearchTerms.length === 0 ) { return items; } const rankedItems = items .map( ( item ) => { return [ item, getItemSearchRank( item, searchInput, config ) ]; } ) .filter( ( [ , rank ] ) => rank > 0 ); rankedItems.sort( ( [ , rank1 ], [ , rank2 ] ) => rank2 - rank1 ); return rankedItems.map( ( [ item ] ) => item ); }; /** * Get the search rank for a given item and a specific search term. * The better the match, the higher the rank. * If the rank equals 0, it should be excluded from the results. * * @param {Object} item Item to filter. * @param {string} searchTerm Search term. * @param {Object} config Search Config. * * @return {number} Search Rank. */ export function getItemSearchRank( item, searchTerm, config = {} ) { const { getName = defaultGetName, getTitle = defaultGetTitle, getDescription = defaultGetDescription, getKeywords = defaultGetKeywords, getCategory = defaultGetCategory, getCollection = defaultGetCollection, } = config; const name = getName( item ); const title = getTitle( item ); const description = getDescription( item ); const keywords = getKeywords( item ); const category = getCategory( item ); const collection = getCollection( item ); const normalizedSearchInput = normalizeString( searchTerm ); const normalizedTitle = normalizeString( title ); let rank = 0; // Prefers exact matches // Then prefers if the beginning of the title matches the search term // name, keywords, categories, collection, variations match come later. if ( normalizedSearchInput === normalizedTitle ) { rank += 30; } else if ( normalizedTitle.startsWith( normalizedSearchInput ) ) { rank += 20; } else { const terms = [ name, title, description, ...keywords, category, collection, ].join( ' ' ); const normalizedSearchTerms = extractWords( normalizedSearchInput ); const unmatchedTerms = removeMatchingTerms( normalizedSearchTerms, terms ); if ( unmatchedTerms.length === 0 ) { rank += 10; } } // Give a better rank to "core" namespaced items. if ( rank !== 0 && name.startsWith( 'core/' ) ) { const isCoreBlockVariation = name !== item.id; // Give a bit better rank to "core" blocks over "core" block variations. rank += isCoreBlockVariation ? 1 : 2; } return rank; }