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
*/
@@ -57,12 +57,12 @@ const _removeChildren = (element) => {
57
57
const _escapeRegExp = ( string ) =>
58
58
string . replace ( / [ . * + \- ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" ) ; // $& means the whole matched string
59
59
60
- const _displayItem = ( item , searchTerms ) => {
60
+ const _displayItem = ( item , searchTerms , highlightTerms ) => {
61
61
const docBuilder = DOCUMENTATION_OPTIONS . BUILDER ;
62
- const docUrlRoot = DOCUMENTATION_OPTIONS . URL_ROOT ;
63
62
const docFileSuffix = DOCUMENTATION_OPTIONS . FILE_SUFFIX ;
64
63
const docLinkSuffix = DOCUMENTATION_OPTIONS . LINK_SUFFIX ;
65
64
const showSearchSummary = DOCUMENTATION_OPTIONS . SHOW_SEARCH_SUMMARY ;
65
+ const contentRoot = document . documentElement . dataset . content_root ;
66
66
67
67
const [ docName , title , anchor , descr , score , _filename ] = item ;
68
68
@@ -75,28 +75,35 @@ const _displayItem = (item, searchTerms) => {
75
75
if ( dirname . match ( / \/ i n d e x \/ $ / ) )
76
76
dirname = dirname . substring ( 0 , dirname . length - 6 ) ;
77
77
else if ( dirname === "index/" ) dirname = "" ;
78
- requestUrl = docUrlRoot + dirname ;
78
+ requestUrl = contentRoot + dirname ;
79
79
linkUrl = requestUrl ;
80
80
} else {
81
81
// normal html builders
82
- requestUrl = docUrlRoot + docName + docFileSuffix ;
82
+ requestUrl = contentRoot + docName + docFileSuffix ;
83
83
linkUrl = docName + docLinkSuffix ;
84
84
}
85
85
let linkEl = listItem . appendChild ( document . createElement ( "a" ) ) ;
86
86
linkEl . href = linkUrl + anchor ;
87
87
linkEl . dataset . score = score ;
88
88
linkEl . innerHTML = title ;
89
- if ( descr )
89
+ if ( descr ) {
90
90
listItem . appendChild ( document . createElement ( "span" ) ) . innerHTML =
91
91
" (" + descr + ")" ;
92
+ // highlight search terms in the description
93
+ if ( SPHINX_HIGHLIGHT_ENABLED ) // set in sphinx_highlight.js
94
+ highlightTerms . forEach ( ( term ) => _highlightText ( listItem , term , "highlighted" ) ) ;
95
+ }
92
96
else if ( showSearchSummary )
93
97
fetch ( requestUrl )
94
98
. then ( ( responseData ) => responseData . text ( ) )
95
99
. then ( ( data ) => {
96
100
if ( data )
97
101
listItem . appendChild (
98
- Search . makeSearchSummary ( data , searchTerms )
102
+ Search . makeSearchSummary ( data , searchTerms , anchor )
99
103
) ;
104
+ // highlight search terms in the summary
105
+ if ( SPHINX_HIGHLIGHT_ENABLED ) // set in sphinx_highlight.js
106
+ highlightTerms . forEach ( ( term ) => _highlightText ( listItem , term , "highlighted" ) ) ;
100
107
} ) ;
101
108
Search . output . appendChild ( listItem ) ;
102
109
} ;
@@ -109,26 +116,43 @@ const _finishSearch = (resultCount) => {
109
116
) ;
110
117
else
111
118
Search . status . innerText = _ (
112
- ` Search finished, found ${ resultCount } page(s) matching the search query.`
113
- ) ;
119
+ " Search finished, found ${resultCount} page(s) matching the search query."
120
+ ) . replace ( '${resultCount}' , resultCount ) ;
114
121
} ;
115
122
const _displayNextItem = (
116
123
results ,
117
124
resultCount ,
118
- searchTerms
125
+ searchTerms ,
126
+ highlightTerms ,
119
127
) => {
120
128
// results left, load the summary and display it
121
129
// this is intended to be dynamic (don't sub resultsCount)
122
130
if ( results . length ) {
123
- _displayItem ( results . pop ( ) , searchTerms ) ;
131
+ _displayItem ( results . pop ( ) , searchTerms , highlightTerms ) ;
124
132
setTimeout (
125
- ( ) => _displayNextItem ( results , resultCount , searchTerms ) ,
133
+ ( ) => _displayNextItem ( results , resultCount , searchTerms , highlightTerms ) ,
126
134
5
127
135
) ;
128
136
}
129
137
// search finished, update title and status message
130
138
else _finishSearch ( resultCount ) ;
131
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
+ } ;
132
156
133
157
/**
134
158
* Default splitQuery function. Can be overridden in ``sphinx.search`` with a
@@ -152,13 +176,26 @@ const Search = {
152
176
_queued_query : null ,
153
177
_pulse_status : - 1 ,
154
178
155
- htmlToText : ( htmlString ) => {
179
+ htmlToText : ( htmlString , anchor ) => {
156
180
const htmlElement = new DOMParser ( ) . parseFromString ( htmlString , 'text/html' ) ;
157
- 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
158
194
const docContent = htmlElement . querySelector ( '[role="main"]' ) ;
159
- if ( docContent !== undefined ) return docContent . textContent ;
195
+ if ( docContent ) return docContent . textContent ;
196
+
160
197
console . warn (
161
- "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."
162
199
) ;
163
200
return "" ;
164
201
} ,
@@ -231,16 +268,7 @@ const Search = {
231
268
else Search . deferQuery ( query ) ;
232
269
} ,
233
270
234
- /**
235
- * execute search (requires search index to be loaded)
236
- */
237
- query : ( query ) => {
238
- const filenames = Search . _index . filenames ;
239
- const docNames = Search . _index . docnames ;
240
- const titles = Search . _index . titles ;
241
- const allTitles = Search . _index . alltitles ;
242
- const indexEntries = Search . _index . indexentries ;
243
-
271
+ _parseQuery : ( query ) => {
244
272
// stem the search terms and add them to the correct list
245
273
const stemmer = new Stemmer ( ) ;
246
274
const searchTerms = new Set ( ) ;
@@ -276,16 +304,32 @@ const Search = {
276
304
// console.info("required: ", [...searchTerms]);
277
305
// console.info("excluded: ", [...excludedTerms]);
278
306
279
- // array of [docname, title, anchor, descr, score, filename]
280
- 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
+
281
325
_removeChildren ( document . getElementById ( "search-progress" ) ) ;
282
326
283
- const queryLower = query . toLowerCase ( ) ;
327
+ const queryLower = query . toLowerCase ( ) . trim ( ) ;
284
328
for ( const [ title , foundTitles ] of Object . entries ( allTitles ) ) {
285
- if ( title . toLowerCase ( ) . includes ( queryLower ) && ( queryLower . length >= title . length / 2 ) ) {
329
+ if ( title . toLowerCase ( ) . trim ( ) . includes ( queryLower ) && ( queryLower . length >= title . length / 2 ) ) {
286
330
for ( const [ file , id ] of foundTitles ) {
287
331
let score = Math . round ( 100 * queryLower . length / title . length )
288
- results . push ( [
332
+ normalResults . push ( [
289
333
docNames [ file ] ,
290
334
titles [ file ] !== title ? `${ titles [ file ] } > ${ title } ` : title ,
291
335
id !== null ? "#" + id : "" ,
@@ -300,46 +344,47 @@ const Search = {
300
344
// search for explicit entries in index directives
301
345
for ( const [ entry , foundEntries ] of Object . entries ( indexEntries ) ) {
302
346
if ( entry . includes ( queryLower ) && ( queryLower . length >= entry . length / 2 ) ) {
303
- for ( const [ file , id ] of foundEntries ) {
304
- let score = Math . round ( 100 * queryLower . length / entry . length )
305
- results . push ( [
347
+ for ( const [ file , id , isMain ] of foundEntries ) {
348
+ const score = Math . round ( 100 * queryLower . length / entry . length ) ;
349
+ const result = [
306
350
docNames [ file ] ,
307
351
titles [ file ] ,
308
352
id ? "#" + id : "" ,
309
353
null ,
310
354
score ,
311
355
filenames [ file ] ,
312
- ] ) ;
356
+ ] ;
357
+ if ( isMain ) {
358
+ normalResults . push ( result ) ;
359
+ } else {
360
+ nonMainIndexResults . push ( result ) ;
361
+ }
313
362
}
314
363
}
315
364
}
316
365
317
366
// lookup as object
318
367
objectTerms . forEach ( ( term ) =>
319
- results . push ( ...Search . performObjectSearch ( term , objectTerms ) )
368
+ normalResults . push ( ...Search . performObjectSearch ( term , objectTerms ) )
320
369
) ;
321
370
322
371
// lookup as search terms in fulltext
323
- results . push ( ...Search . performTermsSearch ( searchTerms , excludedTerms ) ) ;
372
+ normalResults . push ( ...Search . performTermsSearch ( searchTerms , excludedTerms ) ) ;
324
373
325
374
// let the scorer override scores with a custom scoring function
326
- if ( Scorer . score ) results . forEach ( ( item ) => ( item [ 4 ] = Scorer . score ( item ) ) ) ;
327
-
328
- // now sort the results by score (in opposite order of appearance, since the
329
- // display function below uses pop() to retrieve items) and then
330
- // alphabetically
331
- results . sort ( ( a , b ) => {
332
- const leftScore = a [ 4 ] ;
333
- const rightScore = b [ 4 ] ;
334
- if ( leftScore === rightScore ) {
335
- // same score: sort alphabetically
336
- const leftTitle = a [ 1 ] . toLowerCase ( ) ;
337
- const rightTitle = b [ 1 ] . toLowerCase ( ) ;
338
- if ( leftTitle === rightTitle ) return 0 ;
339
- return leftTitle > rightTitle ? - 1 : 1 ; // inverted is intentional
340
- }
341
- return leftScore > rightScore ? 1 : - 1 ;
342
- } ) ;
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 ] ;
343
388
344
389
// remove duplicate search results
345
390
// note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept
@@ -353,14 +398,19 @@ const Search = {
353
398
return acc ;
354
399
} , [ ] ) ;
355
400
356
- 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 ) ;
357
407
358
408
// for debugging
359
409
//Search.lastresults = results.slice(); // a copy
360
410
// console.info("search results:", Search.lastresults);
361
411
362
412
// print the results
363
- _displayNextItem ( results , results . length , searchTerms ) ;
413
+ _displayNextItem ( results , results . length , searchTerms , highlightTerms ) ;
364
414
} ,
365
415
366
416
/**
@@ -458,14 +508,18 @@ const Search = {
458
508
// add support for partial matches
459
509
if ( word . length > 2 ) {
460
510
const escapedWord = _escapeRegExp ( word ) ;
461
- Object . keys ( terms ) . forEach ( ( term ) => {
462
- if ( term . match ( escapedWord ) && ! terms [ word ] )
463
- arr . push ( { files : terms [ term ] , score : Scorer . partialTerm } ) ;
464
- } ) ;
465
- Object . keys ( titleTerms ) . forEach ( ( term ) => {
466
- if ( term . match ( escapedWord ) && ! titleTerms [ word ] )
467
- arr . push ( { files : titleTerms [ word ] , score : Scorer . partialTitle } ) ;
468
- } ) ;
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
+ }
469
523
}
470
524
471
525
// no match but word was a required one
@@ -488,9 +542,8 @@ const Search = {
488
542
489
543
// create the mapping
490
544
files . forEach ( ( file ) => {
491
- if ( fileMap . has ( file ) && fileMap . get ( file ) . indexOf ( word ) === - 1 )
492
- fileMap . get ( file ) . push ( word ) ;
493
- 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 ) ;
494
547
} ) ;
495
548
} ) ;
496
549
@@ -541,8 +594,8 @@ const Search = {
541
594
* search summary for a given text. keywords is a list
542
595
* of stemmed words.
543
596
*/
544
- makeSearchSummary : ( htmlText , keywords ) => {
545
- const text = Search . htmlToText ( htmlText ) ;
597
+ makeSearchSummary : ( htmlText , keywords , anchor ) => {
598
+ const text = Search . htmlToText ( htmlText , anchor ) ;
546
599
if ( text === "" ) return null ;
547
600
548
601
const textLower = text . toLowerCase ( ) ;
0 commit comments