@@ -107,6 +107,239 @@ function setTheme(theme) {
107107 }
108108} ) ( ) ;
109109
110+ // Search functionality.
111+ ( function ( ) {
112+ // Fetch elements.
113+ const searchInput = document . getElementById ( "dei-searchinput" ) ;
114+ const navMain = document . getElementById ( "dei-navmain" ) ;
115+ const navSearch = document . getElementById ( "dei-navsearch" ) ;
116+
117+ if ( ! searchInput | ! navMain || ! navSearch ) {
118+ return ;
119+ }
120+
121+ // Initialize.
122+ const repo = searchInput . dataset . repo ;
123+
124+ let db = null ;
125+ let dbFetch = false ;
126+ let search = null ;
127+
128+ // On input change.
129+ searchInput . addEventListener ( "sl-input" , async ( ) => {
130+ search = searchInput . value . trim ( ) ;
131+
132+ // Cleared; show main navigation again, hide search results.
133+ if ( search === "" ) {
134+ navMain . style = "" ;
135+ navSearch . style = "display: none" ;
136+ return ;
137+ }
138+
139+ // Hide main navigation, show search results.
140+ navMain . style = "display: none" ;
141+ navSearch . style = "" ;
142+
143+ // Load database if not already loaded.
144+ if ( db === null ) {
145+ // Don't fetch more than once.
146+ if ( dbFetch ) {
147+ return ;
148+ }
149+
150+ dbFetch = true ;
151+
152+ // Display spinner.
153+ navSearch . innerHTML = /* HTML */ `
154+ <div class="dei-searchflex">
155+ <sl-spinner></sl-spinner>
156+ </div>
157+ ` ;
158+
159+ // Download and parse database.
160+ const response = await fetch ( `/${ repo } /db.json` ) ;
161+ if ( ! response . ok ) {
162+ // Display error on failure.
163+ navSearch . innerHTML = /* HTML */ `
164+ <div class="dei-searchflex">
165+ <p class="dei-searcherror">
166+ <sl-icon name="exclamation-octagon"></sl-icon><br>
167+ Failed to download database:<br>
168+ ${ response . status } ${ response . statusText }
169+ </p>
170+ </div>
171+ ` ;
172+
173+ // Allow retrying and give up for this attempt.
174+ dbFetch = false ;
175+ return ;
176+ }
177+
178+ db = await response . json ( ) ;
179+ }
180+
181+ // Perform search.
182+ const results = [ ] ;
183+ const terms = search . split ( / / g) ;
184+
185+ for ( const entry of db ) {
186+ // Initialize result.
187+ let result = {
188+ score : 0 ,
189+ titleMatches : [ ] ,
190+ contentMatches : [ ] ,
191+ entry
192+ } ;
193+
194+ // Look for terms.
195+ let termsMatched = { } ;
196+ for ( let term of terms ) {
197+ // Ignore terms shorter than 3 characters.
198+ if ( term . length < 3 ) {
199+ continue ;
200+ }
201+
202+ // Ignore casing.
203+ const title = entry . title . toLowerCase ( ) ;
204+ const content = entry . content . toLowerCase ( ) ;
205+
206+ term = term . toLowerCase ( ) ;
207+
208+ // In title.
209+ let match , index = 0 ;
210+ while ( ( match = title . indexOf ( term , index ) ) != - 1 ) {
211+ // Increase score.
212+ result . score += 5 ;
213+
214+ // Add title match.
215+ const titleMatch = entry . title . substring (
216+ match ,
217+ match + term . length
218+ ) ;
219+
220+ if ( ! ( titleMatch in result . titleMatches ) ) {
221+ result . titleMatches . push ( titleMatch ) ;
222+ }
223+
224+ // Mark term as matched.
225+ termsMatched [ term ] = true ;
226+
227+ index = match + term . length ;
228+ }
229+
230+ // In content.
231+ index = 0 ;
232+ while ( ( match = content . indexOf ( term , index ) ) != - 1 ) {
233+ // Increase core.
234+ result . score ++ ;
235+
236+ // Add content match.
237+ const contentMatch = entry . content . substring (
238+ match ,
239+ match + term . length
240+ ) ;
241+
242+ if ( ! ( contentMatch in result . contentMatches ) ) {
243+ result . contentMatches . push ( contentMatch ) ;
244+ }
245+
246+ termsMatched [ term ] = true ;
247+
248+ index = match + term . length ;
249+ }
250+ }
251+
252+ // Multiply score based on number of unique terms that were matched.
253+ result . score *= Object . entries ( termsMatched ) . length ;
254+
255+ // Result has at least some score; push.
256+ if ( result . score ) {
257+ results . push ( result ) ;
258+ }
259+ }
260+
261+ // Sort by score.
262+ results . sort ( ( lhs , rhs ) => rhs . score - lhs . score ) ;
263+
264+ // No results.
265+ if ( results . length === 0 ) {
266+ navSearch . innerHTML = /* HTML */ `
267+ <div class="dei-searchflex">
268+ <p>Search found no results</p>
269+ </div>
270+ ` ;
271+
272+ return ;
273+ }
274+
275+ // Otherwise generate result HTML.
276+ navSearch . innerHTML = "" ;
277+
278+ for ( let result of results ) {
279+ const entry = result . entry ;
280+
281+ let title = entry . title ;
282+ let preview = entry . content ;
283+
284+ // Mark title matches.
285+ for ( const match of result . titleMatches ) {
286+ title = title . replaceAll ( match , `\x1B\x0B${ match } \x1B\x0C` ) ;
287+ }
288+
289+ // Mark content matches.
290+ for ( const match of result . contentMatches ) {
291+ preview = preview . replaceAll ( match , `\x1B\x0B${ match } \x1B\x0C` ) ;
292+ }
293+
294+ // Limit preview to three highlighted lines.
295+ let lines = 0 ;
296+
297+ preview = preview
298+ . split ( / \n / g)
299+ . filter ( ( x ) => {
300+ if ( lines >= 3 ) {
301+ return false ;
302+ }
303+
304+ if ( x . includes ( "\x1B" ) ) {
305+ lines ++ ;
306+ return true ;
307+ }
308+
309+ return false ;
310+ } )
311+ . join ( "\n" ) ;
312+
313+ // Escape content HTML.
314+ preview = preview
315+ . replaceAll ( "&" , "&" )
316+ . replaceAll ( "<" , "<" )
317+ . replaceAll ( ">" , ">" )
318+ . replaceAll ( "\"" , """ )
319+ . replaceAll ( "'" , "'" ) ;
320+
321+ // Perform replacements.
322+ title = title
323+ . replaceAll ( "\x1B\x0B" , `<span class="dei-searchhighlight">` )
324+ . replaceAll ( "\x1B\x0C" , `</span>` ) ;
325+ preview = preview
326+ . replaceAll ( "\x1B\x0B" , `<span class="dei-searchhighlight">` )
327+ . replaceAll ( "\x1B\x0C" , `</span>` )
328+ . replaceAll ( "\n" , "<br>" ) ;
329+
330+ // Append HTML.
331+ navSearch . innerHTML += /* HTML */ `
332+ <li>
333+ <a href="${ entry . href } ">
334+ ${ title }
335+ <p>${ preview } </p>
336+ </a>
337+ </li>
338+ ` ;
339+ }
340+ } ) ;
341+ } ) ( ) ;
342+
110343// Scroll to current page in navigation when present.
111344window . addEventListener ( "load" , ( ) => {
112345 // Fetch current page in navigation; do nothing if there's none.
0 commit comments