@wordpress/core-data 7.0.1 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/build/entity-types/helpers.js.map +1 -1
- package/build/entity-types/plugin.js.map +1 -1
- package/build/fetch/__experimental-fetch-link-suggestions.js +89 -118
- package/build/fetch/__experimental-fetch-link-suggestions.js.map +1 -1
- package/build-module/entity-types/helpers.js.map +1 -1
- package/build-module/entity-types/plugin.js.map +1 -1
- package/build-module/fetch/__experimental-fetch-link-suggestions.js +86 -118
- package/build-module/fetch/__experimental-fetch-link-suggestions.js.map +1 -1
- package/build-types/entity-types/helpers.d.ts +1 -1
- package/build-types/entity-types/plugin.d.ts +1 -1
- package/build-types/entity-types/plugin.d.ts.map +1 -1
- package/build-types/fetch/__experimental-fetch-link-suggestions.d.ts +48 -84
- package/build-types/fetch/__experimental-fetch-link-suggestions.d.ts.map +1 -1
- package/package.json +17 -17
- package/src/entity-types/helpers.ts +1 -1
- package/src/entity-types/plugin.ts +1 -1
- package/src/fetch/__experimental-fetch-link-suggestions.ts +296 -0
- package/src/fetch/test/__experimental-fetch-link-suggestions.js +95 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/src/fetch/__experimental-fetch-link-suggestions.js +0 -237
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import apiFetch from '@wordpress/api-fetch';
|
|
5
|
+
import { addQueryArgs } from '@wordpress/url';
|
|
6
|
+
import { decodeEntities } from '@wordpress/html-entities';
|
|
7
|
+
import { __ } from '@wordpress/i18n';
|
|
8
|
+
|
|
9
|
+
export type SearchOptions = {
|
|
10
|
+
/**
|
|
11
|
+
* Displays initial search suggestions, when true.
|
|
12
|
+
*/
|
|
13
|
+
isInitialSuggestions?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Search options for initial suggestions.
|
|
16
|
+
*/
|
|
17
|
+
initialSuggestionsSearchOptions?: Omit<
|
|
18
|
+
SearchOptions,
|
|
19
|
+
'isInitialSuggestions' | 'initialSuggestionsSearchOptions'
|
|
20
|
+
>;
|
|
21
|
+
/**
|
|
22
|
+
* Filters by search type.
|
|
23
|
+
*/
|
|
24
|
+
type?: 'attachment' | 'post' | 'term' | 'post-format';
|
|
25
|
+
/**
|
|
26
|
+
* Slug of the post-type or taxonomy.
|
|
27
|
+
*/
|
|
28
|
+
subtype?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Which page of results to return.
|
|
31
|
+
*/
|
|
32
|
+
page?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Search results per page.
|
|
35
|
+
*/
|
|
36
|
+
perPage?: number;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type EditorSettings = {
|
|
40
|
+
/**
|
|
41
|
+
* Disables post formats, when true.
|
|
42
|
+
*/
|
|
43
|
+
disablePostFormats?: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type SearchAPIResult = {
|
|
47
|
+
id: number;
|
|
48
|
+
title: string;
|
|
49
|
+
url: string;
|
|
50
|
+
type: string;
|
|
51
|
+
subtype: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type MediaAPIResult = {
|
|
55
|
+
id: number;
|
|
56
|
+
title: { rendered: string };
|
|
57
|
+
source_url: string;
|
|
58
|
+
type: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type SearchResult = {
|
|
62
|
+
/**
|
|
63
|
+
* Post or term id.
|
|
64
|
+
*/
|
|
65
|
+
id: number;
|
|
66
|
+
/**
|
|
67
|
+
* Link url.
|
|
68
|
+
*/
|
|
69
|
+
url: string;
|
|
70
|
+
/**
|
|
71
|
+
* Title of the link.
|
|
72
|
+
*/
|
|
73
|
+
title: string;
|
|
74
|
+
/**
|
|
75
|
+
* The taxonomy or post type slug or type URL.
|
|
76
|
+
*/
|
|
77
|
+
type: string;
|
|
78
|
+
/**
|
|
79
|
+
* Link kind of post-type or taxonomy
|
|
80
|
+
*/
|
|
81
|
+
kind?: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Fetches link suggestions from the WordPress API.
|
|
86
|
+
*
|
|
87
|
+
* WordPress does not support searching multiple tables at once, e.g. posts and terms, so we
|
|
88
|
+
* perform multiple queries at the same time and then merge the results together.
|
|
89
|
+
*
|
|
90
|
+
* @param search
|
|
91
|
+
* @param searchOptions
|
|
92
|
+
* @param editorSettings
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```js
|
|
96
|
+
* import { __experimentalFetchLinkSuggestions as fetchLinkSuggestions } from '@wordpress/core-data';
|
|
97
|
+
*
|
|
98
|
+
* //...
|
|
99
|
+
*
|
|
100
|
+
* export function initialize( id, settings ) {
|
|
101
|
+
*
|
|
102
|
+
* settings.__experimentalFetchLinkSuggestions = (
|
|
103
|
+
* search,
|
|
104
|
+
* searchOptions
|
|
105
|
+
* ) => fetchLinkSuggestions( search, searchOptions, settings );
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export default async function fetchLinkSuggestions(
|
|
109
|
+
search: string,
|
|
110
|
+
searchOptions: SearchOptions = {},
|
|
111
|
+
editorSettings: EditorSettings = {}
|
|
112
|
+
): Promise< SearchResult[] > {
|
|
113
|
+
const searchOptionsToUse =
|
|
114
|
+
searchOptions.isInitialSuggestions &&
|
|
115
|
+
searchOptions.initialSuggestionsSearchOptions
|
|
116
|
+
? {
|
|
117
|
+
...searchOptions,
|
|
118
|
+
...searchOptions.initialSuggestionsSearchOptions,
|
|
119
|
+
}
|
|
120
|
+
: searchOptions;
|
|
121
|
+
|
|
122
|
+
const {
|
|
123
|
+
type,
|
|
124
|
+
subtype,
|
|
125
|
+
page,
|
|
126
|
+
perPage = searchOptions.isInitialSuggestions ? 3 : 20,
|
|
127
|
+
} = searchOptionsToUse;
|
|
128
|
+
|
|
129
|
+
const { disablePostFormats = false } = editorSettings;
|
|
130
|
+
|
|
131
|
+
const queries: Promise< SearchResult[] >[] = [];
|
|
132
|
+
|
|
133
|
+
if ( ! type || type === 'post' ) {
|
|
134
|
+
queries.push(
|
|
135
|
+
apiFetch< SearchAPIResult[] >( {
|
|
136
|
+
path: addQueryArgs( '/wp/v2/search', {
|
|
137
|
+
search,
|
|
138
|
+
page,
|
|
139
|
+
per_page: perPage,
|
|
140
|
+
type: 'post',
|
|
141
|
+
subtype,
|
|
142
|
+
} ),
|
|
143
|
+
} )
|
|
144
|
+
.then( ( results ) => {
|
|
145
|
+
return results.map( ( result ) => {
|
|
146
|
+
return {
|
|
147
|
+
id: result.id,
|
|
148
|
+
url: result.url,
|
|
149
|
+
title:
|
|
150
|
+
decodeEntities( result.title || '' ) ||
|
|
151
|
+
__( '(no title)' ),
|
|
152
|
+
type: result.subtype || result.type,
|
|
153
|
+
kind: 'post-type',
|
|
154
|
+
};
|
|
155
|
+
} );
|
|
156
|
+
} )
|
|
157
|
+
.catch( () => [] ) // Fail by returning no results.
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if ( ! type || type === 'term' ) {
|
|
162
|
+
queries.push(
|
|
163
|
+
apiFetch< SearchAPIResult[] >( {
|
|
164
|
+
path: addQueryArgs( '/wp/v2/search', {
|
|
165
|
+
search,
|
|
166
|
+
page,
|
|
167
|
+
per_page: perPage,
|
|
168
|
+
type: 'term',
|
|
169
|
+
subtype,
|
|
170
|
+
} ),
|
|
171
|
+
} )
|
|
172
|
+
.then( ( results ) => {
|
|
173
|
+
return results.map( ( result ) => {
|
|
174
|
+
return {
|
|
175
|
+
id: result.id,
|
|
176
|
+
url: result.url,
|
|
177
|
+
title:
|
|
178
|
+
decodeEntities( result.title || '' ) ||
|
|
179
|
+
__( '(no title)' ),
|
|
180
|
+
type: result.subtype || result.type,
|
|
181
|
+
kind: 'taxonomy',
|
|
182
|
+
};
|
|
183
|
+
} );
|
|
184
|
+
} )
|
|
185
|
+
.catch( () => [] ) // Fail by returning no results.
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if ( ! disablePostFormats && ( ! type || type === 'post-format' ) ) {
|
|
190
|
+
queries.push(
|
|
191
|
+
apiFetch< SearchAPIResult[] >( {
|
|
192
|
+
path: addQueryArgs( '/wp/v2/search', {
|
|
193
|
+
search,
|
|
194
|
+
page,
|
|
195
|
+
per_page: perPage,
|
|
196
|
+
type: 'post-format',
|
|
197
|
+
subtype,
|
|
198
|
+
} ),
|
|
199
|
+
} )
|
|
200
|
+
.then( ( results ) => {
|
|
201
|
+
return results.map( ( result ) => {
|
|
202
|
+
return {
|
|
203
|
+
id: result.id,
|
|
204
|
+
url: result.url,
|
|
205
|
+
title:
|
|
206
|
+
decodeEntities( result.title || '' ) ||
|
|
207
|
+
__( '(no title)' ),
|
|
208
|
+
type: result.subtype || result.type,
|
|
209
|
+
kind: 'taxonomy',
|
|
210
|
+
};
|
|
211
|
+
} );
|
|
212
|
+
} )
|
|
213
|
+
.catch( () => [] ) // Fail by returning no results.
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if ( ! type || type === 'attachment' ) {
|
|
218
|
+
queries.push(
|
|
219
|
+
apiFetch< MediaAPIResult[] >( {
|
|
220
|
+
path: addQueryArgs( '/wp/v2/media', {
|
|
221
|
+
search,
|
|
222
|
+
page,
|
|
223
|
+
per_page: perPage,
|
|
224
|
+
} ),
|
|
225
|
+
} )
|
|
226
|
+
.then( ( results ) => {
|
|
227
|
+
return results.map( ( result ) => {
|
|
228
|
+
return {
|
|
229
|
+
id: result.id,
|
|
230
|
+
url: result.source_url,
|
|
231
|
+
title:
|
|
232
|
+
decodeEntities( result.title.rendered || '' ) ||
|
|
233
|
+
__( '(no title)' ),
|
|
234
|
+
type: result.type,
|
|
235
|
+
kind: 'media',
|
|
236
|
+
};
|
|
237
|
+
} );
|
|
238
|
+
} )
|
|
239
|
+
.catch( () => [] ) // Fail by returning no results.
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const responses = await Promise.all( queries );
|
|
244
|
+
|
|
245
|
+
let results = responses.flat();
|
|
246
|
+
results = results.filter( ( result ) => !! result.id );
|
|
247
|
+
results = sortResults( results, search );
|
|
248
|
+
results = results.slice( 0, perPage );
|
|
249
|
+
return results;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Sort search results by relevance to the given query.
|
|
254
|
+
*
|
|
255
|
+
* Sorting is necessary as we're querying multiple endpoints and merging the results. For example
|
|
256
|
+
* a taxonomy title might be more relevant than a post title, but by default taxonomy results will
|
|
257
|
+
* be ordered after all the (potentially irrelevant) post results.
|
|
258
|
+
*
|
|
259
|
+
* We sort by scoring each result, where the score is the number of tokens in the title that are
|
|
260
|
+
* also in the search query, divided by the total number of tokens in the title. This gives us a
|
|
261
|
+
* score between 0 and 1, where 1 is a perfect match.
|
|
262
|
+
*
|
|
263
|
+
* @param results
|
|
264
|
+
* @param search
|
|
265
|
+
*/
|
|
266
|
+
export function sortResults( results: SearchResult[], search: string ) {
|
|
267
|
+
const searchTokens = new Set( tokenize( search ) );
|
|
268
|
+
|
|
269
|
+
const scores = {};
|
|
270
|
+
for ( const result of results ) {
|
|
271
|
+
if ( result.title ) {
|
|
272
|
+
const titleTokens = tokenize( result.title );
|
|
273
|
+
const matchingTokens = titleTokens.filter( ( token ) =>
|
|
274
|
+
searchTokens.has( token )
|
|
275
|
+
);
|
|
276
|
+
scores[ result.id ] = matchingTokens.length / titleTokens.length;
|
|
277
|
+
} else {
|
|
278
|
+
scores[ result.id ] = 0;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return results.sort( ( a, b ) => scores[ b.id ] - scores[ a.id ] );
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Turns text into an array of tokens, with whitespace and punctuation removed.
|
|
287
|
+
*
|
|
288
|
+
* For example, `"I'm having a ball."` becomes `[ "im", "having", "a", "ball" ]`.
|
|
289
|
+
*
|
|
290
|
+
* @param text
|
|
291
|
+
*/
|
|
292
|
+
export function tokenize( text: string ): string[] {
|
|
293
|
+
// \p{L} matches any kind of letter from any language.
|
|
294
|
+
// \p{N} matches any kind of numeric character.
|
|
295
|
+
return text.toLowerCase().match( /[\p{L}\p{N}]+/gu ) || [];
|
|
296
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Internal dependencies
|
|
3
3
|
*/
|
|
4
|
-
import
|
|
4
|
+
import {
|
|
5
|
+
default as fetchLinkSuggestions,
|
|
6
|
+
sortResults,
|
|
7
|
+
tokenize,
|
|
8
|
+
} from '../__experimental-fetch-link-suggestions';
|
|
5
9
|
|
|
6
10
|
jest.mock( '@wordpress/api-fetch', () =>
|
|
7
11
|
jest.fn( ( { path } ) => {
|
|
@@ -317,3 +321,93 @@ describe( 'fetchLinkSuggestions', () => {
|
|
|
317
321
|
);
|
|
318
322
|
} );
|
|
319
323
|
} );
|
|
324
|
+
|
|
325
|
+
describe( 'sortResults', () => {
|
|
326
|
+
it( 'returns empty array for empty results', () => {
|
|
327
|
+
expect( sortResults( [], '' ) ).toEqual( [] );
|
|
328
|
+
} );
|
|
329
|
+
|
|
330
|
+
it( 'orders results', () => {
|
|
331
|
+
const results = [
|
|
332
|
+
{
|
|
333
|
+
id: 1,
|
|
334
|
+
title: 'How to get from Stockholm to Helsinki by boat',
|
|
335
|
+
url: 'http://wordpress.local/stockholm-helsinki-boat/',
|
|
336
|
+
type: 'page',
|
|
337
|
+
kind: 'post-type',
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
id: 2,
|
|
341
|
+
title: 'A day trip from Stockholm to Swedish countryside towns',
|
|
342
|
+
url: 'http://wordpress.local/day-trip-stockholm/',
|
|
343
|
+
type: 'page',
|
|
344
|
+
kind: 'post-type',
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
id: 3,
|
|
348
|
+
title: 'The art of packing lightly: How to travel with just a cabin bag',
|
|
349
|
+
url: 'http://wordpress.local/packing-lightly/',
|
|
350
|
+
type: 'page',
|
|
351
|
+
kind: 'post-type',
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
id: 4,
|
|
355
|
+
title: 'Tips for travel with a young baby',
|
|
356
|
+
url: 'http://wordpress.local/young-baby-tips/',
|
|
357
|
+
type: 'page',
|
|
358
|
+
kind: 'post-type',
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
id: 5,
|
|
362
|
+
title: '', // Test that empty titles don't cause an error.
|
|
363
|
+
url: 'http://wordpress.local/420/',
|
|
364
|
+
type: 'page',
|
|
365
|
+
kind: 'post-type',
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
id: 6,
|
|
369
|
+
title: 'City Guides',
|
|
370
|
+
url: 'http://wordpress.local/city-guides/',
|
|
371
|
+
type: 'category',
|
|
372
|
+
kind: 'taxonomy',
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
id: 7,
|
|
376
|
+
title: 'Travel Tips',
|
|
377
|
+
url: 'http://wordpress.local/travel-tips/',
|
|
378
|
+
type: 'category',
|
|
379
|
+
kind: 'taxonomy',
|
|
380
|
+
},
|
|
381
|
+
];
|
|
382
|
+
const order = sortResults( results, 'travel tips' ).map(
|
|
383
|
+
( result ) => result.id
|
|
384
|
+
);
|
|
385
|
+
expect( order ).toEqual( [
|
|
386
|
+
7, // exact match
|
|
387
|
+
4, // contains: travel, tips
|
|
388
|
+
3, // contains: travel
|
|
389
|
+
// same order as input:
|
|
390
|
+
1,
|
|
391
|
+
2,
|
|
392
|
+
5,
|
|
393
|
+
6,
|
|
394
|
+
] );
|
|
395
|
+
} );
|
|
396
|
+
} );
|
|
397
|
+
|
|
398
|
+
describe( 'tokenize', () => {
|
|
399
|
+
it( 'returns empty array for empty string', () => {
|
|
400
|
+
expect( tokenize( '' ) ).toEqual( [] );
|
|
401
|
+
} );
|
|
402
|
+
|
|
403
|
+
it( 'tokenizes a string', () => {
|
|
404
|
+
expect( tokenize( 'Hello, world!' ) ).toEqual( [ 'hello', 'world' ] );
|
|
405
|
+
} );
|
|
406
|
+
|
|
407
|
+
it( 'tokenizes non latin languages', () => {
|
|
408
|
+
expect( tokenize( 'こんにちは、世界!' ) ).toEqual( [
|
|
409
|
+
'こんにちは',
|
|
410
|
+
'世界',
|
|
411
|
+
] );
|
|
412
|
+
} );
|
|
413
|
+
} );
|