4
4
*
5
5
* Sphinx JavaScript utilities for the full-text search.
6
6
*
7
- * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS.
7
+ * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS.
8
8
* :license: BSD, see LICENSE for details.
9
9
*
10
10
*/
@@ -99,7 +99,7 @@ const _displayItem = (item, searchTerms, highlightTerms) => {
99
99
. then ( ( data ) => {
100
100
if ( data )
101
101
listItem . appendChild (
102
- Search . makeSearchSummary ( data , searchTerms )
102
+ Search . makeSearchSummary ( data , searchTerms , anchor )
103
103
) ;
104
104
// highlight search terms in the summary
105
105
if ( SPHINX_HIGHLIGHT_ENABLED ) // set in sphinx_highlight.js
@@ -116,8 +116,8 @@ const _finishSearch = (resultCount) => {
116
116
) ;
117
117
else
118
118
Search . status . innerText = _ (
119
- ` Search finished, found ${ resultCount } page(s) matching the search query.`
120
- ) ;
119
+ " Search finished, found ${resultCount} page(s) matching the search query."
120
+ ) . replace ( '${resultCount}' , resultCount ) ;
121
121
} ;
122
122
const _displayNextItem = (
123
123
results ,
@@ -137,6 +137,22 @@ const _displayNextItem = (
137
137
// search finished, update title and status message
138
138
else _finishSearch ( resultCount ) ;
139
139
} ;
140
+ // Helper function used by query() to order search results.
141
+ // Each input is an array of [docname, title, anchor, descr, score, filename].
142
+ // Order the results by score (in opposite order of appearance, since the
143
+ // `_displayNextItem` function uses pop() to retrieve items) and then alphabetically.
144
+ const _orderResultsByScoreThenName = ( a , b ) => {
145
+ const leftScore = a [ 4 ] ;
146
+ const rightScore = b [ 4 ] ;
147
+ if ( leftScore === rightScore ) {
148
+ // same score: sort alphabetically
149
+ const leftTitle = a [ 1 ] . toLowerCase ( ) ;
150
+ const rightTitle = b [ 1 ] . toLowerCase ( ) ;
151
+ if ( leftTitle === rightTitle ) return 0 ;
152
+ return leftTitle > rightTitle ? - 1 : 1 ; // inverted is intentional
153
+ }
154
+ return leftScore > rightScore ? 1 : - 1 ;
155
+ } ;
140
156
141
157
/**
142
158
* Default splitQuery function. Can be overridden in ``sphinx.search`` with a
@@ -160,13 +176,26 @@ const Search = {
160
176
_queued_query : null ,
161
177
_pulse_status : - 1 ,
162
178
163
- htmlToText : ( htmlString ) => {
179
+ htmlToText : ( htmlString , anchor ) => {
164
180
const htmlElement = new DOMParser ( ) . parseFromString ( htmlString , 'text/html' ) ;
165
- htmlElement . querySelectorAll ( ".headerlink" ) . forEach ( ( el ) => { el . remove ( ) } ) ;
181
+ for ( const removalQuery of [ ".headerlinks" , "script" , "style" ] ) {
182
+ htmlElement . querySelectorAll ( removalQuery ) . forEach ( ( el ) => { el . remove ( ) } ) ;
183
+ }
184
+ if ( anchor ) {
185
+ const anchorContent = htmlElement . querySelector ( `[role="main"] ${ anchor } ` ) ;
186
+ if ( anchorContent ) return anchorContent . textContent ;
187
+
188
+ console . warn (
189
+ `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${ anchor } '. Check your theme or template.`
190
+ ) ;
191
+ }
192
+
193
+ // if anchor not specified or not found, fall back to main content
166
194
const docContent = htmlElement . querySelector ( '[role="main"]' ) ;
167
- if ( docContent !== undefined ) return docContent . textContent ;
195
+ if ( docContent ) return docContent . textContent ;
196
+
168
197
console . warn (
169
- "Content block not found. Sphinx search tries to obtain it via '[role=main]'. Could you check your theme or template."
198
+ "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template."
170
199
) ;
171
200
return "" ;
172
201
} ,
@@ -239,16 +268,7 @@ const Search = {
239
268
else Search . deferQuery ( query ) ;
240
269
} ,
241
270
242
- /**
243
- * execute search (requires search index to be loaded)
244
- */
245
- query : ( query ) => {
246
- const filenames = Search . _index . filenames ;
247
- const docNames = Search . _index . docnames ;
248
- const titles = Search . _index . titles ;
249
- const allTitles = Search . _index . alltitles ;
250
- const indexEntries = Search . _index . indexentries ;
251
-
271
+ _parseQuery : ( query ) => {
252
272
// stem the search terms and add them to the correct list
253
273
const stemmer = new Stemmer ( ) ;
254
274
const searchTerms = new Set ( ) ;
@@ -284,16 +304,32 @@ const Search = {
284
304
// console.info("required: ", [...searchTerms]);
285
305
// console.info("excluded: ", [...excludedTerms]);
286
306
287
- // array of [docname, title, anchor, descr, score, filename]
288
- let results = [ ] ;
307
+ return [ query , searchTerms , excludedTerms , highlightTerms , objectTerms ] ;
308
+ } ,
309
+
310
+ /**
311
+ * execute search (requires search index to be loaded)
312
+ */
313
+ _performSearch : ( query , searchTerms , excludedTerms , highlightTerms , objectTerms ) => {
314
+ const filenames = Search . _index . filenames ;
315
+ const docNames = Search . _index . docnames ;
316
+ const titles = Search . _index . titles ;
317
+ const allTitles = Search . _index . alltitles ;
318
+ const indexEntries = Search . _index . indexentries ;
319
+
320
+ // Collect multiple result groups to be sorted separately and then ordered.
321
+ // Each is an array of [docname, title, anchor, descr, score, filename].
322
+ const normalResults = [ ] ;
323
+ const nonMainIndexResults = [ ] ;
324
+
289
325
_removeChildren ( document . getElementById ( "search-progress" ) ) ;
290
326
291
- const queryLower = query . toLowerCase ( ) ;
327
+ const queryLower = query . toLowerCase ( ) . trim ( ) ;
292
328
for ( const [ title , foundTitles ] of Object . entries ( allTitles ) ) {
293
- if ( title . toLowerCase ( ) . includes ( queryLower ) && ( queryLower . length >= title . length / 2 ) ) {
329
+ if ( title . toLowerCase ( ) . trim ( ) . includes ( queryLower ) && ( queryLower . length >= title . length / 2 ) ) {
294
330
for ( const [ file , id ] of foundTitles ) {
295
331
let score = Math . round ( 100 * queryLower . length / title . length )
296
- results . push ( [
332
+ normalResults . push ( [
297
333
docNames [ file ] ,
298
334
titles [ file ] !== title ? `${ titles [ file ] } > ${ title } ` : title ,
299
335
id !== null ? "#" + id : "" ,
@@ -308,46 +344,47 @@ const Search = {
308
344
// search for explicit entries in index directives
309
345
for ( const [ entry , foundEntries ] of Object . entries ( indexEntries ) ) {
310
346
if ( entry . includes ( queryLower ) && ( queryLower . length >= entry . length / 2 ) ) {
311
- for ( const [ file , id ] of foundEntries ) {
312
- let score = Math . round ( 100 * queryLower . length / entry . length )
313
- results . push ( [
347
+ for ( const [ file , id , isMain ] of foundEntries ) {
348
+ const score = Math . round ( 100 * queryLower . length / entry . length ) ;
349
+ const result = [
314
350
docNames [ file ] ,
315
351
titles [ file ] ,
316
352
id ? "#" + id : "" ,
317
353
null ,
318
354
score ,
319
355
filenames [ file ] ,
320
- ] ) ;
356
+ ] ;
357
+ if ( isMain ) {
358
+ normalResults . push ( result ) ;
359
+ } else {
360
+ nonMainIndexResults . push ( result ) ;
361
+ }
321
362
}
322
363
}
323
364
}
324
365
325
366
// lookup as object
326
367
objectTerms . forEach ( ( term ) =>
327
- results . push ( ...Search . performObjectSearch ( term , objectTerms ) )
368
+ normalResults . push ( ...Search . performObjectSearch ( term , objectTerms ) )
328
369
) ;
329
370
330
371
// lookup as search terms in fulltext
331
- results . push ( ...Search . performTermsSearch ( searchTerms , excludedTerms ) ) ;
372
+ normalResults . push ( ...Search . performTermsSearch ( searchTerms , excludedTerms ) ) ;
332
373
333
374
// let the scorer override scores with a custom scoring function
334
- if ( Scorer . score ) results . forEach ( ( item ) => ( item [ 4 ] = Scorer . score ( item ) ) ) ;
335
-
336
- // now sort the results by score (in opposite order of appearance, since the
337
- // display function below uses pop() to retrieve items) and then
338
- // alphabetically
339
- results . sort ( ( a , b ) => {
340
- const leftScore = a [ 4 ] ;
341
- const rightScore = b [ 4 ] ;
342
- if ( leftScore === rightScore ) {
343
- // same score: sort alphabetically
344
- const leftTitle = a [ 1 ] . toLowerCase ( ) ;
345
- const rightTitle = b [ 1 ] . toLowerCase ( ) ;
346
- if ( leftTitle === rightTitle ) return 0 ;
347
- return leftTitle > rightTitle ? - 1 : 1 ; // inverted is intentional
348
- }
349
- return leftScore > rightScore ? 1 : - 1 ;
350
- } ) ;
375
+ if ( Scorer . score ) {
376
+ normalResults . forEach ( ( item ) => ( item [ 4 ] = Scorer . score ( item ) ) ) ;
377
+ nonMainIndexResults . forEach ( ( item ) => ( item [ 4 ] = Scorer . score ( item ) ) ) ;
378
+ }
379
+
380
+ // Sort each group of results by score and then alphabetically by name.
381
+ normalResults . sort ( _orderResultsByScoreThenName ) ;
382
+ nonMainIndexResults . sort ( _orderResultsByScoreThenName ) ;
383
+
384
+ // Combine the result groups in (reverse) order.
385
+ // Non-main index entries are typically arbitrary cross-references,
386
+ // so display them after other results.
387
+ let results = [ ...nonMainIndexResults , ...normalResults ] ;
351
388
352
389
// remove duplicate search results
353
390
// note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept
@@ -361,7 +398,12 @@ const Search = {
361
398
return acc ;
362
399
} , [ ] ) ;
363
400
364
- results = results . reverse ( ) ;
401
+ return results . reverse ( ) ;
402
+ } ,
403
+
404
+ query : ( query ) => {
405
+ const [ searchQuery , searchTerms , excludedTerms , highlightTerms , objectTerms ] = Search . _parseQuery ( query ) ;
406
+ const results = Search . _performSearch ( searchQuery , searchTerms , excludedTerms , highlightTerms , objectTerms ) ;
365
407
366
408
// for debugging
367
409
//Search.lastresults = results.slice(); // a copy
@@ -466,14 +508,18 @@ const Search = {
466
508
// add support for partial matches
467
509
if ( word . length > 2 ) {
468
510
const escapedWord = _escapeRegExp ( word ) ;
469
- Object . keys ( terms ) . forEach ( ( term ) => {
470
- if ( term . match ( escapedWord ) && ! terms [ word ] )
471
- arr . push ( { files : terms [ term ] , score : Scorer . partialTerm } ) ;
472
- } ) ;
473
- Object . keys ( titleTerms ) . forEach ( ( term ) => {
474
- if ( term . match ( escapedWord ) && ! titleTerms [ word ] )
475
- arr . push ( { files : titleTerms [ word ] , score : Scorer . partialTitle } ) ;
476
- } ) ;
511
+ if ( ! terms . hasOwnProperty ( word ) ) {
512
+ Object . keys ( terms ) . forEach ( ( term ) => {
513
+ if ( term . match ( escapedWord ) )
514
+ arr . push ( { files : terms [ term ] , score : Scorer . partialTerm } ) ;
515
+ } ) ;
516
+ }
517
+ if ( ! titleTerms . hasOwnProperty ( word ) ) {
518
+ Object . keys ( titleTerms ) . forEach ( ( term ) => {
519
+ if ( term . match ( escapedWord ) )
520
+ arr . push ( { files : titleTerms [ term ] , score : Scorer . partialTitle } ) ;
521
+ } ) ;
522
+ }
477
523
}
478
524
479
525
// no match but word was a required one
@@ -496,9 +542,8 @@ const Search = {
496
542
497
543
// create the mapping
498
544
files . forEach ( ( file ) => {
499
- if ( fileMap . has ( file ) && fileMap . get ( file ) . indexOf ( word ) === - 1 )
500
- fileMap . get ( file ) . push ( word ) ;
501
- else fileMap . set ( file , [ word ] ) ;
545
+ if ( ! fileMap . has ( file ) ) fileMap . set ( file , [ word ] ) ;
546
+ else if ( fileMap . get ( file ) . indexOf ( word ) === - 1 ) fileMap . get ( file ) . push ( word ) ;
502
547
} ) ;
503
548
} ) ;
504
549
@@ -549,8 +594,8 @@ const Search = {
549
594
* search summary for a given text. keywords is a list
550
595
* of stemmed words.
551
596
*/
552
- makeSearchSummary : ( htmlText , keywords ) => {
553
- const text = Search . htmlToText ( htmlText ) ;
597
+ makeSearchSummary : ( htmlText , keywords , anchor ) => {
598
+ const text = Search . htmlToText ( htmlText , anchor ) ;
554
599
if ( text === "" ) return null ;
555
600
556
601
const textLower = text . toLowerCase ( ) ;
0 commit comments