44 *
55 * Sphinx JavaScript utilities for the full-text search.
66 *
7- * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS.
7+ * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS.
88 * :license: BSD, see LICENSE for details.
99 *
1010 */
@@ -99,7 +99,7 @@ const _displayItem = (item, searchTerms, highlightTerms) => {
9999 . then ( ( data ) => {
100100 if ( data )
101101 listItem . appendChild (
102- Search . makeSearchSummary ( data , searchTerms )
102+ Search . makeSearchSummary ( data , searchTerms , anchor )
103103 ) ;
104104 // highlight search terms in the summary
105105 if ( SPHINX_HIGHLIGHT_ENABLED ) // set in sphinx_highlight.js
@@ -116,8 +116,8 @@ const _finishSearch = (resultCount) => {
116116 ) ;
117117 else
118118 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 ) ;
121121} ;
122122const _displayNextItem = (
123123 results ,
@@ -137,6 +137,22 @@ const _displayNextItem = (
137137 // search finished, update title and status message
138138 else _finishSearch ( resultCount ) ;
139139} ;
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+ } ;
140156
141157/**
142158 * Default splitQuery function. Can be overridden in ``sphinx.search`` with a
@@ -160,13 +176,26 @@ const Search = {
160176 _queued_query : null ,
161177 _pulse_status : - 1 ,
162178
163- htmlToText : ( htmlString ) => {
179+ htmlToText : ( htmlString , anchor ) => {
164180 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
166194 const docContent = htmlElement . querySelector ( '[role="main"]' ) ;
167- if ( docContent !== undefined ) return docContent . textContent ;
195+ if ( docContent ) return docContent . textContent ;
196+
168197 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."
170199 ) ;
171200 return "" ;
172201 } ,
@@ -239,16 +268,7 @@ const Search = {
239268 else Search . deferQuery ( query ) ;
240269 } ,
241270
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 ) => {
252272 // stem the search terms and add them to the correct list
253273 const stemmer = new Stemmer ( ) ;
254274 const searchTerms = new Set ( ) ;
@@ -284,16 +304,32 @@ const Search = {
284304 // console.info("required: ", [...searchTerms]);
285305 // console.info("excluded: ", [...excludedTerms]);
286306
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+
289325 _removeChildren ( document . getElementById ( "search-progress" ) ) ;
290326
291- const queryLower = query . toLowerCase ( ) ;
327+ const queryLower = query . toLowerCase ( ) . trim ( ) ;
292328 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 ) ) {
294330 for ( const [ file , id ] of foundTitles ) {
295331 let score = Math . round ( 100 * queryLower . length / title . length )
296- results . push ( [
332+ normalResults . push ( [
297333 docNames [ file ] ,
298334 titles [ file ] !== title ? `${ titles [ file ] } > ${ title } ` : title ,
299335 id !== null ? "#" + id : "" ,
@@ -308,46 +344,47 @@ const Search = {
308344 // search for explicit entries in index directives
309345 for ( const [ entry , foundEntries ] of Object . entries ( indexEntries ) ) {
310346 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 = [
314350 docNames [ file ] ,
315351 titles [ file ] ,
316352 id ? "#" + id : "" ,
317353 null ,
318354 score ,
319355 filenames [ file ] ,
320- ] ) ;
356+ ] ;
357+ if ( isMain ) {
358+ normalResults . push ( result ) ;
359+ } else {
360+ nonMainIndexResults . push ( result ) ;
361+ }
321362 }
322363 }
323364 }
324365
325366 // lookup as object
326367 objectTerms . forEach ( ( term ) =>
327- results . push ( ...Search . performObjectSearch ( term , objectTerms ) )
368+ normalResults . push ( ...Search . performObjectSearch ( term , objectTerms ) )
328369 ) ;
329370
330371 // lookup as search terms in fulltext
331- results . push ( ...Search . performTermsSearch ( searchTerms , excludedTerms ) ) ;
372+ normalResults . push ( ...Search . performTermsSearch ( searchTerms , excludedTerms ) ) ;
332373
333374 // 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 ] ;
351388
352389 // remove duplicate search results
353390 // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept
@@ -361,7 +398,12 @@ const Search = {
361398 return acc ;
362399 } , [ ] ) ;
363400
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 ) ;
365407
366408 // for debugging
367409 //Search.lastresults = results.slice(); // a copy
@@ -466,14 +508,18 @@ const Search = {
466508 // add support for partial matches
467509 if ( word . length > 2 ) {
468510 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+ }
477523 }
478524
479525 // no match but word was a required one
@@ -496,9 +542,8 @@ const Search = {
496542
497543 // create the mapping
498544 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 ) ;
502547 } ) ;
503548 } ) ;
504549
@@ -549,8 +594,8 @@ const Search = {
549594 * search summary for a given text. keywords is a list
550595 * of stemmed words.
551596 */
552- makeSearchSummary : ( htmlText , keywords ) => {
553- const text = Search . htmlToText ( htmlText ) ;
597+ makeSearchSummary : ( htmlText , keywords , anchor ) => {
598+ const text = Search . htmlToText ( htmlText , anchor ) ;
554599 if ( text === "" ) return null ;
555600
556601 const textLower = text . toLowerCase ( ) ;
0 commit comments