@@ -27,6 +27,53 @@ document.addEventListener("DOMContentLoaded", function () {
2727 return `${ renderedAndHiddenText } ${ metadataText } ` ;
2828 } ;
2929
30+ const normalizeWhitespace = ( text ) => {
31+ return ( text || "" ) . toLowerCase ( ) . trim ( ) . replace ( / \s + / g, " " ) ;
32+ } ;
33+
34+ const stripWrappingQuotes = ( text ) => {
35+ return ( text || "" ) . replace ( / ^ " ( .+ ) " $ / , "$1" ) ;
36+ } ;
37+
38+ const normalizeSearchTokens = ( searchTerm ) => {
39+ return normalizeWhitespace ( stripWrappingQuotes ( searchTerm ) ) . split ( " " ) . filter ( Boolean ) ;
40+ } ;
41+
42+ const escapeRegex = ( text ) => text . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" ) ;
43+
44+ const hasOrderedPhraseMatch = ( searchableText , tokens ) => {
45+ if ( tokens . length <= 1 ) {
46+ return true ;
47+ }
48+
49+ // Allow optional middle initials between tokens, e.g. "jeremy a. collins".
50+ const optionalInitials = "(?:[\\s,.-]+[a-z]\\.?)*" ;
51+ const separator = `${ optionalInitials } [\\s,.-]+` ;
52+ const pattern = tokens . map ( ( token ) => `\\b${ escapeRegex ( token ) } \\b` ) . join ( separator ) ;
53+ return new RegExp ( pattern , "i" ) . test ( searchableText ) ;
54+ } ;
55+
56+ const matchesSearchTerm = ( searchableText , searchTerm ) => {
57+ const normalizedSearchableText = normalizeWhitespace ( searchableText ) ;
58+ const tokens = normalizeSearchTokens ( searchTerm ) ;
59+
60+ if ( tokens . length === 0 ) {
61+ return true ;
62+ }
63+
64+ const hasAllTokens = tokens . every ( ( token ) => normalizedSearchableText . includes ( token ) ) ;
65+ if ( ! hasAllTokens ) {
66+ return false ;
67+ }
68+
69+ // For multi-word input, require phrase-like order with support for middle initials.
70+ if ( tokens . length > 1 ) {
71+ return hasOrderedPhraseMatch ( normalizedSearchableText , tokens ) ;
72+ }
73+
74+ return true ;
75+ } ;
76+
3077 // actual bibsearch logic
3178 const filterItems = ( searchTerm ) => {
3279 document . querySelectorAll ( ".bibliography, .unloaded" ) . forEach ( ( element ) => element . classList . remove ( "unloaded" ) ) ;
@@ -39,15 +86,15 @@ document.addEventListener("DOMContentLoaded", function () {
3986 }
4087 nonMatchingElements . forEach ( ( element ) => {
4188 const searchableText = getSearchableText ( element ) ;
42- if ( searchableText . indexOf ( searchTerm ) === - 1 ) {
89+ if ( ! matchesSearchTerm ( searchableText , searchTerm ) ) {
4390 element . classList . add ( "unloaded" ) ;
4491 }
4592 } ) ;
4693 } else {
4794 // Simply add unloaded class to all non-matching items if Browser does not support CSS highlights
4895 document . querySelectorAll ( ".bibliography > li" ) . forEach ( ( element ) => {
4996 const searchableText = getSearchableText ( element ) ;
50- if ( searchableText . indexOf ( searchTerm ) === - 1 ) {
97+ if ( ! matchesSearchTerm ( searchableText , searchTerm ) ) {
5198 element . classList . add ( "unloaded" ) ;
5299 }
53100 } ) ;
0 commit comments