From 57b73a5f47d69d695fba57ec966ae7dd25400a66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weber?= Date: Sun, 27 Oct 2024 12:37:05 +0100 Subject: [PATCH] search: improve handling of multiterm search #407 --- layouts/partials/version.txt | 2 +- static/js/search.js | 26 +++++++++++++--- static/js/theme.js | 59 ++++++++++++++++++++++++++---------- 3 files changed, 66 insertions(+), 21 deletions(-) diff --git a/layouts/partials/version.txt b/layouts/partials/version.txt index 049300afad..696c577f67 100644 --- a/layouts/partials/version.txt +++ b/layouts/partials/version.txt @@ -1 +1 @@ -7.1.1+fc960aa5eaf2240ba8a6de92943ac838423e1269 \ No newline at end of file +7.1.1+8debc83a770553885b78318e2651a127a82d2b0c \ No newline at end of file diff --git a/static/js/search.js b/static/js/search.js index bf36418589..5dc26aaae2 100644 --- a/static/js/search.js +++ b/static/js/search.js @@ -3,6 +3,10 @@ import { init, search } from './orama-adapter.js'; (function(){ +function escapeRegex( string ){ + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + window.relearn = window.relearn || {}; window.relearn.executeInitialSearch = @@ -95,12 +99,19 @@ function executeSearch( value ) { a.forEach( function(item){ var page = item.page; var context = []; - if( item.matches ){ + if( item.matches && item.matches.length ){ var numContextWords = 10; var contextPattern = '(?:\\S+ +){0,' + numContextWords + '}\\S*\\b(?:' + - item.matches.map( function(match){return match.replace(/\W/g, '\\$&')} ).join('|') + + escapeRegex( item.matches[0] ) + ')\\b\\S*(?: +\\S+){0,' + numContextWords + '}'; context = page.content.match(new RegExp(contextPattern, 'i')); + if( !context ){ + item.matches.shift(); + var contextPattern = '(?:\\S+ +){0,' + numContextWords + '}\\S*\\b(?:' + + item.matches.map( escapeRegex ).join('|') + + ')\\b\\S*(?: +\\S+){0,' + numContextWords + '}'; + context = page.content.match(new RegExp(contextPattern, 'i')); + } } var divsuggestion = document.createElement('a'); divsuggestion.className = 'autocomplete-suggestion'; @@ -169,12 +180,19 @@ function initSearchAfterLoad(){ renderItem: function( item, term ) { var page = item.page; var context = []; - if( item.matches ){ + if( item.matches && item.matches.length ){ var numContextWords = 2; var contextPattern = '(?:\\S+ +){0,' + numContextWords + '}\\S*\\b(?:' + - item.matches.map( function(match){return match.replace(/\W/g, '\\$&')} ).join('|') + + escapeRegex( item.matches[0] ) + ')\\b\\S*(?: +\\S+){0,' + numContextWords + '}'; context = page.content.match(new RegExp(contextPattern, 'i')); + if( !context ){ + item.matches.shift(); + var contextPattern = '(?:\\S+ +){0,' + numContextWords + '}\\S*\\b(?:' + + item.matches.map( escapeRegex ).join('|') + + ')\\b\\S*(?: +\\S+){0,' + numContextWords + '}'; + context = page.content.match(new RegExp(contextPattern, 'i')); + } } var divsuggestion = document.createElement('div'); divsuggestion.className = 'autocomplete-suggestion'; diff --git a/static/js/theme.js b/static/js/theme.js index e241d2ce91..89bfb5068f 100644 --- a/static/js/theme.js +++ b/static/js/theme.js @@ -1327,10 +1327,12 @@ function scrollToPositions() { return; } - var search = sessionStorage.getItem( window.relearn.absBaseUri+'/search-value' ); - if( search && search.length ){ - search = regexEscape( search ); - var found = elementContains( search, elc ); + + var value = sessionStorage.getItem( window.relearn.absBaseUri+'/search-value' ); + var words = (value ?? '') .split( ' ' ).filter( word => word.trim() != '' ); + if( words && words.length ){ + unmark(); + var found = elementContains( words, elc ); var searchedElem = found.length && found[ 0 ]; if( searchedElem ){ searchedElem.scrollIntoView(); @@ -1339,6 +1341,8 @@ function scrollToPositions() { window.scroll( 0, scrolledY - 125 ); } } + sessionStorage.setItem( window.relearn.absBaseUri+'/search-value', value ); + mark(); return; } @@ -1371,7 +1375,7 @@ function mark() { bodyInnerLinks[i].classList.add( 'highlight' ); } - var value = sessionStorage.getItem( window.relearn.absBaseUri + '/search-value' ); + var value = (sessionStorage.getItem( window.relearn.absBaseUri + '/search-value' ) ?? '').split( ' ' ).filter( word => word.trim() != '' ); var highlightableElements = document.querySelectorAll( '.highlightable' ); highlight( highlightableElements, value, { element: 'mark', className: 'search' } ); @@ -1411,17 +1415,10 @@ function highlight( es, words, options ){ }; Object.assign( settings, options ); - if( !words ){ return; } - if( words.constructor === String ){ - words = [ words ]; - } - words = words.filter( function( word, i ){ - return word != ''; - }); + if( !words.length ){ return; } words = words.map( function( word, i ){ return regexEscape( word ); }); - if( words.length == 0 ){ return this; } var flag = settings.caseSensitive ? '' : 'i'; var pattern = "(" + words.join( '|' ) + ')'; @@ -1507,8 +1504,24 @@ function unhighlight( es, options ){ }; // replace jQuery.createPseudo with https://stackoverflow.com/a/66318392 -function elementContains( txt, e ){ - var regex = RegExp( txt, 'i' ); +function elementContains( words, e ){ + var settings = { + caseSensitive: false, + wordsOnly: false + }; + + if( !words.length ){ return; } + words = words.map( function( word, i ){ + return regexEscape( word ); + }); + + var flag = settings.caseSensitive ? '' : 'i'; + var pattern = "(" + words.join( '\\s+' ) + ')'; + if( settings.wordsOnly ){ + pattern = '\\b' + pattern + '\\b'; + } + var regex = new RegExp( pattern, flag ); + var nodes = []; if( e ){ var tree = document.createTreeWalker( e, 4 /* NodeFilter.SHOW_TEXT */, function( node ){ @@ -1518,6 +1531,20 @@ function elementContains( txt, e ){ while( node = tree.nextNode() ){ nodes.push( node.parentElement ); } + + var pattern = "(" + words.join( '|' ) + ')'; + if( settings.wordsOnly ){ + pattern = '\\b' + pattern + '\\b'; + } + regex = new RegExp( pattern, flag ); + + tree = document.createTreeWalker( e, 4 /* NodeFilter.SHOW_TEXT */, function( node ){ + return regex.test( node.data ); + }, false ); + node = null; + while( node = tree.nextNode() ){ + nodes.push( node.parentElement ); + } } return nodes; } @@ -1537,7 +1564,7 @@ function initSearch() { e.addEventListener( 'keydown', function( event ){ if( event.key == 'Escape' ){ var input = event.target; - var search = sessionStorage.getItem( window.relearn.absBaseUri+'/search-value' ); + var search = (sessionStorage.getItem( window.relearn.absBaseUri+'/search-value' ) ?? '').split( ' ' ).filter( word => word.trim() != '' ); if( !search || !search.length ){ input.blur(); }