@@ -19,6 +19,13 @@ import Fuse from 'fuse.js';
1919 var RECENT_KEY = 'cfd_recent_searches' ;
2020 var RECENT_MAX = 5 ;
2121 var SUGGEST_SCORE_THRESHOLD = 0.35 ;
22+ var REMOTE_CACHE_KEY = 'cfd_remote_search_v1' ;
23+ var REMOTE_CACHE_VERSION = 1 ;
24+ var REMOTE_CACHE_TTL_MS = 30 * 60 * 1000 ;
25+ var REMOTE_DESCRIPTION_MAX = 200 ;
26+ var REMOTE_CONTENT_EXCERPT = 300 ;
27+ var indexByUrl = new Map ( ) ;
28+ var remoteLoadStarted = false ;
2229
2330 var FUSE_OPTIONS = {
2431 keys : [
@@ -171,51 +178,200 @@ import Fuse from 'fuse.js';
171178 return item ;
172179 }
173180
174- function buildIndex ( data ) {
175- var items = [ ] ;
176- var indexByUrl = new Map ( ) ;
177-
178- function push ( item ) {
179- if ( item . hidden ) return ;
180- var key = normalizeUrl ( item . url ) ;
181- if ( ! key ) return ;
182- if ( indexByUrl . has ( key ) ) {
183- mergeItemMetadata ( indexByUrl . get ( key ) , item ) ;
184- return ;
185- }
186- indexByUrl . set ( key , item ) ;
187- items . push ( item ) ;
181+ function pushItem ( item ) {
182+ if ( ! item || item . hidden ) return ;
183+ var key = normalizeUrl ( item . url ) ;
184+ if ( ! key ) return ;
185+ if ( indexByUrl . has ( key ) ) {
186+ mergeItemMetadata ( indexByUrl . get ( key ) , item ) ;
187+ return ;
188188 }
189+ indexByUrl . set ( key , item ) ;
190+ allItems . push ( item ) ;
191+ }
192+
193+ function stripHtml ( html ) {
194+ return String ( html || '' )
195+ . replace ( / < [ ^ > ] + > / g, ' ' )
196+ . replace ( / & (?: # x ? [ \d a - f ] + | \w + ) ; / gi, ' ' )
197+ . replace ( / \s + / g, ' ' )
198+ . trim ( ) ;
199+ }
200+
201+ function mapRemotePost ( post , source ) {
202+ if ( ! post || ! post . title || ! post . permalink ) return null ;
203+ var summary = stripHtml ( post . summary || '' ) ;
204+ var excerpt = summary || stripHtml ( post . content || '' ) . slice ( 0 , REMOTE_CONTENT_EXCERPT ) ;
205+ var description = excerpt . slice ( 0 , REMOTE_DESCRIPTION_MAX ) ;
206+ return mapItem (
207+ {
208+ name : post . title ,
209+ url : post . permalink ,
210+ description : description ,
211+ phosphorIcon : source . icon || 'article' ,
212+ external : false ,
213+ } ,
214+ source . category ,
215+ source . type
216+ ) ;
217+ }
218+
219+ function itemFromCache ( raw ) {
220+ var item = {
221+ name : raw . name ,
222+ url : raw . url ,
223+ category : raw . category ,
224+ type : raw . type ,
225+ description : raw . description || '' ,
226+ phosphorIcon : raw . phosphorIcon || 'article' ,
227+ external : false ,
228+ aliases : raw . aliases || [ ] ,
229+ searchText : raw . searchText || '' ,
230+ } ;
231+ if ( ! item . searchText ) item . searchText = buildSearchText ( item ) ;
232+ return item ;
233+ }
234+
235+ function slimRemoteItem ( item ) {
236+ return {
237+ name : item . name ,
238+ url : item . url ,
239+ category : item . category ,
240+ type : item . type ,
241+ description : item . description ,
242+ phosphorIcon : item . phosphorIcon ,
243+ aliases : item . aliases ,
244+ searchText : item . searchText ,
245+ } ;
246+ }
247+
248+ function readRemoteCache ( ) {
249+ try {
250+ var raw = sessionStorage . getItem ( REMOTE_CACHE_KEY ) ;
251+ if ( ! raw ) return null ;
252+ var parsed = JSON . parse ( raw ) ;
253+ if ( ! parsed || parsed . v !== REMOTE_CACHE_VERSION ) return null ;
254+ if ( Date . now ( ) - parsed . fetchedAt > REMOTE_CACHE_TTL_MS ) return null ;
255+ if ( ! Array . isArray ( parsed . items ) ) return null ;
256+ return parsed . items ;
257+ } catch ( e ) {
258+ return null ;
259+ }
260+ }
261+
262+ function writeRemoteCache ( items ) {
263+ try {
264+ sessionStorage . setItem (
265+ REMOTE_CACHE_KEY ,
266+ JSON . stringify ( {
267+ v : REMOTE_CACHE_VERSION ,
268+ fetchedAt : Date . now ( ) ,
269+ items : items . map ( slimRemoteItem ) ,
270+ } )
271+ ) ;
272+ } catch ( e ) {
273+ /* ignore quota errors */
274+ }
275+ }
276+
277+ function rebuildFuse ( ) {
278+ fuse = allItems . length ? buildFuseIndex ( allItems ) : null ;
279+ }
280+
281+ function refreshSearchIfOpen ( ) {
282+ if ( isOpen && input && input . value . trim ( ) ) {
283+ runSearch ( input . value ) ;
284+ }
285+ }
286+
287+ function mergeRemoteItems ( items ) {
288+ if ( ! items || ! items . length ) return ;
289+ items . forEach ( function ( item ) {
290+ pushItem ( item ) ;
291+ } ) ;
292+ rebuildFuse ( ) ;
293+ refreshSearchIfOpen ( ) ;
294+ }
295+
296+ function fetchRemoteSource ( source ) {
297+ return fetch ( source . url , { credentials : 'same-origin' } )
298+ . then ( function ( response ) {
299+ return response . ok ? response . json ( ) : [ ] ;
300+ } )
301+ . then ( function ( rows ) {
302+ return ( rows || [ ] )
303+ . map ( function ( post ) {
304+ return mapRemotePost ( post , source ) ;
305+ } )
306+ . filter ( Boolean ) ;
307+ } )
308+ . catch ( function ( ) {
309+ return [ ] ;
310+ } ) ;
311+ }
312+
313+ function loadRemoteSources ( sources ) {
314+ if ( ! sources || ! sources . length || remoteLoadStarted ) return ;
315+ remoteLoadStarted = true ;
316+
317+ var cached = readRemoteCache ( ) ;
318+ if ( cached && cached . length ) {
319+ mergeRemoteItems (
320+ cached . map ( function ( raw ) {
321+ return itemFromCache ( raw ) ;
322+ } )
323+ ) ;
324+ }
325+
326+ Promise . all (
327+ sources . map ( function ( source ) {
328+ return fetchRemoteSource ( source ) ;
329+ } )
330+ ) . then ( function ( groups ) {
331+ var remoteItems = [ ] ;
332+ groups . forEach ( function ( group ) {
333+ remoteItems = remoteItems . concat ( group ) ;
334+ } ) ;
335+ if ( remoteItems . length ) {
336+ writeRemoteCache ( remoteItems ) ;
337+ mergeRemoteItems ( remoteItems ) ;
338+ }
339+ } ) ;
340+ }
341+
342+ function buildIndex ( data ) {
343+ allItems = [ ] ;
344+ indexByUrl = new Map ( ) ;
189345
190346 if ( data . home && data . home . categories ) {
191347 data . home . categories . forEach ( function ( cat ) {
192348 ( cat . items || [ ] ) . forEach ( function ( item ) {
193- push ( mapItem ( item , cat . name , 'tool' ) ) ;
349+ pushItem ( mapItem ( item , cat . name , 'tool' ) ) ;
194350 } ) ;
195351 } ) ;
196352 }
197353 if ( data . ai && data . ai . data ) {
198354 data . ai . data . forEach ( function ( item ) {
199- push ( mapItem ( item , 'AI Tools' , 'ai' ) ) ;
355+ pushItem ( mapItem ( item , 'AI Tools' , 'ai' ) ) ;
200356 } ) ;
201357 }
202358 if ( data . games && data . games . data ) {
203359 data . games . data . forEach ( function ( item ) {
204- push ( mapItem ( item , 'Games' , 'game' ) ) ;
360+ pushItem ( mapItem ( item , 'Games' , 'game' ) ) ;
205361 } ) ;
206362 }
207363 if ( data . designlab && data . designlab . data ) {
208364 data . designlab . data . forEach ( function ( item ) {
209- push ( mapItem ( item , 'Design Lab' , 'design' ) ) ;
365+ pushItem ( mapItem ( item , 'Design Lab' , 'design' ) ) ;
210366 } ) ;
211367 }
212368 if ( data . store && data . store . data ) {
213369 data . store . data . forEach ( function ( item ) {
214- push ( mapItem ( item , 'Store' , 'store' ) ) ;
370+ pushItem ( mapItem ( item , 'Store' , 'store' ) ) ;
215371 } ) ;
216372 }
217373
218- return items ;
374+ return allItems ;
219375 }
220376
221377 function buildFuseIndex ( items ) {
@@ -746,13 +902,16 @@ import Fuse from 'fuse.js';
746902 }
747903
748904 if ( window . CFD_SEARCH && window . CFD_SEARCH . data ) {
749- allItems = buildIndex ( window . CFD_SEARCH . data ) ;
750- fuse = buildFuseIndex ( allItems ) ;
905+ buildIndex ( window . CFD_SEARCH . data ) ;
906+ rebuildFuse ( ) ;
751907 } else {
752908 allItems = [ ] ;
909+ indexByUrl = new Map ( ) ;
753910 fuse = null ;
754911 }
755912
913+ loadRemoteSources ( window . CFD_SEARCH && window . CFD_SEARCH . remoteSources ) ;
914+
756915 mountPaletteToBody ( ) ;
757916 initialized = true ;
758917
0 commit comments