diff --git a/app/frontend/src/Controller/Search/Result.js b/app/frontend/src/Controller/Search/Result.js
new file mode 100644
index 00000000..2d54b260
--- /dev/null
+++ b/app/frontend/src/Controller/Search/Result.js
@@ -0,0 +1,136 @@
+import React, { forwardRef, useContext } from 'react'
+import { string, number, shape, bool, func } from 'prop-types'
+import classNames from 'classnames'
+import { ListItem } from '@material-ui/core'
+
+import controller from '../../lib/controller'
+import { getTranslation, getTransliteration, customiseLine } from '../../lib/utils'
+import { WritersContext, RecommendedSourcesContext, SettingsContext } from '../../lib/contexts'
+import { LANGUAGE_NAMES, SOURCE_ABBREVIATIONS } from '../../lib/consts'
+
+/**
+ * Renders a single result, highlighting the match.
+ * @param {string} gurmukhi The shabad line to display.
+ * @param {int} typeId The type id of line.
+ * @param {string} lineId The id of the line.
+ * @param {string} shabadId The id of the shabad.
+ * @param {Component} ref The ref to the component.
+ * @param {int} sourceId The id of source.
+ * @param {Object} shabad The object containng section information and other metadata.
+ * @param {int} sourcePage The page number of shabad in source.
+ * @param {string} translations The translations of shabad line to display.
+ * @param {string} transliterations The transliterations of shabad line to display.
+ */
+const Result = forwardRef( ( {
+ gurmukhi,
+ typeId,
+ id: lineId,
+ shabadId,
+ sourceId,
+ shabad,
+ focused,
+ highlighter,
+ sourcePage,
+ translations,
+ transliterations,
+}, ref ) => {
+ const { local: {
+ sources,
+ search: {
+ showResultCitations,
+ resultTransliterationLanguage,
+ resultTranslationLanguage,
+ lineEnding,
+ },
+ } = {} } = useContext( SettingsContext )
+
+ const writers = useContext( WritersContext )
+ const recommendedSources = useContext( RecommendedSourcesContext )
+
+ const transliteration = resultTransliterationLanguage && transliterations && customiseLine(
+ getTransliteration(
+ { transliterations },
+ resultTransliterationLanguage,
+ ),
+ { lineEnding, typeId },
+ )
+
+ const translation = resultTranslationLanguage && translations && customiseLine(
+ getTranslation( {
+ line: { translations },
+ shabad: { sourceId },
+ recommendedSources,
+ sources,
+ languageId: resultTranslationLanguage,
+ } ),
+ { lineEnding, typeId },
+ )
+
+ // Separate the line into words before the match, the match, and after the match
+ const highlight = highlighter( { gurmukhi } )
+ const [ beforeMatch, match, afterMatch ] = highlight( gurmukhi )
+ const [ translitBeforeMatch, translitMatch, translitAfterMatch ] = highlight( transliteration )
+
+ // Send the shabad id and line id to the server on click
+ const onClick = () => controller.shabad( { shabadId, lineId } )
+
+ // Helper render functions for citation
+ const showCitation = showResultCitations && shabad && shabad.section
+ const getEnglish = ( { nameEnglish } ) => nameEnglish
+ const getWriterName = () => getEnglish( writers[ shabad.writerId ] )
+ const getPageName = () => recommendedSources[ shabad.sourceId ].pageNameEnglish
+
+ return (
+
+
+
+ {beforeMatch && {beforeMatch}}
+ {match && {match}}
+ {afterMatch && {afterMatch}}
+
+
+
+ {translation && (
+
+ {translation}
+
+ )}
+
+ {transliteration && (
+
+ {translitBeforeMatch && {translitBeforeMatch}}
+ {translitMatch && {translitMatch}}
+ {translitAfterMatch && {translitAfterMatch}}
+
+ )}
+
+
+ {showCitation && (
+
+ {[
+ getWriterName(),
+ SOURCE_ABBREVIATIONS[ sourceId ],
+ `${getPageName()} ${sourcePage}`,
+ ].reduce( ( prev, curr ) => [ prev, ' - ', curr ] )}
+
+ )}
+
+
+ )
+} )
+
+Result.propTypes = {
+ gurmukhi: string.isRequired,
+ id: string.isRequired,
+ typeId: string.isRequired,
+ shabadId: string.isRequired,
+ focused: bool.isRequired,
+ highlighter: func.isRequired,
+ sourceId: number.isRequired,
+ shabad: shape( { } ).isRequired,
+ sourcePage: number.isRequired,
+ translations: string.isRequired,
+ transliterations: string.isRequired,
+}
+
+export default Result
diff --git a/app/frontend/src/Controller/Search/Results.js b/app/frontend/src/Controller/Search/Results.js
deleted file mode 100644
index b5b0b9cb..00000000
--- a/app/frontend/src/Controller/Search/Results.js
+++ /dev/null
@@ -1,177 +0,0 @@
-import React, { useContext } from 'react'
-import classNames from 'classnames'
-import { ListItem, List } from '@material-ui/core'
-import { string, oneOfType, number, instanceOf, shape, arrayOf, func } from 'prop-types'
-
-import controller from '../../lib/controller'
-import { getTranslation, getTransliteration, customiseLine } from '../../lib/utils'
-import { WritersContext, RecommendedSourcesContext, SettingsContext } from '../../lib/contexts'
-import { SEARCH_TYPES, LANGUAGE_NAMES, SEARCH_ANCHORS, SOURCE_ABBREVIATIONS } from '../../lib/consts'
-
-import highlightMatches from './highlight-matches'
-
-const Result = ( { results, searchedValue, anchor, register, focused } ) => {
- const { local: {
- sources,
- search: {
- showResultCitations,
- resultTransliterationLanguage,
- resultTranslationLanguage,
- lineEnding,
- },
- } = {} } = useContext( SettingsContext )
-
- const writers = useContext( WritersContext )
- const recommendedSources = useContext( RecommendedSourcesContext )
-
- /**
- * Renders a single result, highlighting the match.
- * @param {string} gurmukhi The shabad line to display.
- * @param {int} typeId The type id of line.
- * @param {string} lineId The id of the line.
- * @param {string} shabadId The id of the shabad.
- * @param {Component} ref The ref to the component.
- * @param {int} sourceId The id of source.
- * @param {Object} shabad The object containng section information and other metadata.
- * @param {int} sourcePage The page number of shabad in source.
- * @param {string} translations The translations of shabad line to display.
- * @param {string} transliterations The transliterations of shabad line to display.
- */
- const ResultList = ( {
- gurmukhi,
- typeId,
- id: lineId,
- shabadId,
- ref,
- sourceId,
- shabad,
- sourcePage,
- translations,
- transliterations,
- } ) => {
- const transliteration = resultTransliterationLanguage && transliterations && customiseLine(
- getTransliteration(
- { transliterations },
- resultTransliterationLanguage,
- ),
- { lineEnding, typeId },
- )
-
- const translation = resultTranslationLanguage && translations && customiseLine(
- getTranslation( {
- line: { translations },
- shabad: { sourceId },
- recommendedSources,
- sources,
- languageId: resultTranslationLanguage,
- } ),
- { lineEnding, typeId },
- )
-
- // Grab the search mode or assume it's first letter
- const mode = SEARCH_ANCHORS[ anchor ] || SEARCH_TYPES.firstLetter
-
- // Separate the line into words before the match, the match, and after the match
- const getMatches = highlightMatches( gurmukhi )
-
- const [ beforeMatch, match, afterMatch ] = getMatches(
- gurmukhi,
- searchedValue,
- mode,
- )
- const [ translitBeforeMatch, translitMatch, translitAfterMatch ] = getMatches(
- transliteration,
- searchedValue,
- mode,
- )
-
- // Send the shabad id and line id to the server on click
- const onClick = () => controller.shabad( { shabadId, lineId } )
-
- // Helper render functions for citation
- const showCitation = showResultCitations && shabad && shabad.section
- const getEnglish = ( { nameEnglish } ) => nameEnglish
- const getWriterName = () => getEnglish( writers[ shabad.writerId ] )
- const getPageName = () => recommendedSources[ shabad.sourceId ].pageNameEnglish
-
- return (
-
-
-
- {beforeMatch && {beforeMatch}}
- {match && {match}}
- {afterMatch && {afterMatch}}
-
-
-
- {translation && (
-
- {translation}
-
- )}
-
- {transliteration && (
-
- {translitBeforeMatch && {translitBeforeMatch}}
- {translitMatch && {translitMatch}}
- {translitAfterMatch && {translitAfterMatch}}
-
- )}
-
-
- {showCitation && (
-
- {[
- getWriterName(),
- SOURCE_ABBREVIATIONS[ sourceId ],
- `${getPageName()} ${sourcePage}`,
- ].reduce( ( prev, curr ) => [ prev, ' - ', curr ] )}
-
- )}
-
-
- )
- }
-
- ResultList.propTypes = {
- gurmukhi: string.isRequired,
- id: string.isRequired,
- typeId: string.isRequired,
- shabadId: string.isRequired,
- ref: instanceOf( Result ).isRequired,
- sourceId: number.isRequired,
- shabad: shape( { } ).isRequired,
- sourcePage: number.isRequired,
- translations: string.isRequired,
- transliterations: string.isRequired,
- }
-
- return (
-
- {results
- ? results.map( ( props, i ) => ResultList( {
- ...props,
- ref: c => register( i, c ),
- focused: focused === i,
- } ) )
- : ''}
-
- )
-}
-
-Result.propTypes = {
- results: arrayOf( shape( {} ) ),
- searchedValue: string,
- anchor: string,
- register: func.isRequired,
- focused: oneOfType( [ string, number ] ),
-}
-
-Result.defaultProps = {
- results: [],
- searchedValue: undefined,
- anchor: undefined,
- focused: undefined,
-}
-
-export default Result
diff --git a/app/frontend/src/Controller/Search/highlight-matches.js b/app/frontend/src/Controller/Search/highlight-matches.js
deleted file mode 100644
index b5146471..00000000
--- a/app/frontend/src/Controller/Search/highlight-matches.js
+++ /dev/null
@@ -1,57 +0,0 @@
-const { stripVishraams, toAscii, firstLetters, stripAccents, toUnicode } = require( 'gurmukhi-utils' )
-const { SEARCH_TYPES } = require( '../../lib/consts' )
-
-const fullWordMatches = ( line, query ) => {
- const sanitisedQuery = query.trim()
-
- const foundPosition = line.search( sanitisedQuery )
- const matchStartPosition = line.lastIndexOf( ' ', foundPosition )
-
- const wordEndPosition = line.indexOf( ' ', foundPosition + sanitisedQuery.length )
- // If the match finishes in the last word, no space will be deteced, and wordEndPosition
- // will be -1. In this case, we want to end at the last position in the line.
- const matchEndPosition = wordEndPosition === -1 ? line.length - 1 : wordEndPosition
-
- return [
- line.substring( 0, matchStartPosition ),
- line.substring( matchStartPosition, matchEndPosition ),
- line.substring( matchEndPosition ),
- ]
-}
-
-const firstLetterMatches = ( line, query ) => {
- const baseLine = stripVishraams( line )
-
- const letters = toAscii( firstLetters( stripAccents( toUnicode( baseLine ) ) ) )
- const words = baseLine.split( ' ' )
-
- const startPosition = letters.search( stripAccents( query ) )
- const endPosition = startPosition + query.length
-
- return [
- `${words.slice( 0, startPosition ).join( ' ' )} `,
- `${words.slice( startPosition, endPosition ).join( ' ' )} `,
- `${words.slice( endPosition ).join( ' ' )} `,
- ]
-}
-
-/**
- * Separates the line into words before the first match, the first match, and after the match.
- * @param value The full line.
- * @param input The string inputted by the user.
- * @param mode The type of search being performed, either first word or full word.
- * @return An array of [ beforeMatch, match, afterMatch ],
- * with `match` being the highlighted section.`.
- */
-const highlightMatches = gurmukhi => ( value, input, mode ) => {
- if ( !value ) return [ '', '', '' ]
-
- // Account for wildcard characters
- const sanitizedInput = input.replace( new RegExp( '_', 'g' ), '.' )
-
- return mode === SEARCH_TYPES.fullWord
- ? fullWordMatches( gurmukhi, sanitizedInput )
- : firstLetterMatches( value, sanitizedInput )
-}
-
-export default highlightMatches
diff --git a/app/frontend/src/Controller/Search/index.js b/app/frontend/src/Controller/Search/index.js
index 80485a9f..564d7085 100644
--- a/app/frontend/src/Controller/Search/index.js
+++ b/app/frontend/src/Controller/Search/index.js
@@ -5,7 +5,7 @@ import { useLocation, useHistory } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faTimes } from '@fortawesome/free-solid-svg-icons'
import { stringify } from 'querystring'
-import { Input, InputAdornment, IconButton } from '@material-ui/core'
+import { Input, InputAdornment, IconButton, List } from '@material-ui/core'
import { getUrlState } from '../../lib/utils'
import { SettingsContext } from '../../lib/contexts'
@@ -18,7 +18,8 @@ import {
MIN_SEARCH_CHARS,
} from '../../lib/consts'
-import Result from './Results'
+import Result from './Result'
+import getHighlighter from './match-highlighter'
import './index.css'
// Generate the regex for capturing anchor chars, optionally
@@ -147,6 +148,10 @@ const Search = ( { updateFocus, register, focused } ) => {
useEffect( () => { highlightSearch() }, [] )
+ // Get match highlighter for the current search mode
+ const searchMode = SEARCH_ANCHORS[ anchor ] || SEARCH_TYPES.firstLetter
+ const highlighter = getHighlighter( searchedValue, searchMode )
+
return (
{
autoComplete: 'off',
}}
/>
-
+
+
+ {results && results.map( ( props, i ) => (
+ register( i, c )}
+ focused={focused === i}
+ highlighter={highlighter}
+ />
+ ) )}
+
)
}
diff --git a/app/frontend/src/Controller/Search/match-highlighter.js b/app/frontend/src/Controller/Search/match-highlighter.js
new file mode 100644
index 00000000..1fab8181
--- /dev/null
+++ b/app/frontend/src/Controller/Search/match-highlighter.js
@@ -0,0 +1,77 @@
+import { stripVishraams, toAscii, firstLetters, stripAccents, toUnicode } from 'gurmukhi-utils'
+
+import { SEARCH_TYPES } from '../../lib/consts'
+
+const fullWordMatches = query => ( { target, gurmukhi } ) => {
+ const baseGurmukhi = stripVishraams( gurmukhi )
+ const baseTarget = stripVishraams( target )
+
+ const sanitisedQuery = query.trim()
+
+ const foundPosition = baseGurmukhi.search( sanitisedQuery )
+ const matchStartPosition = baseGurmukhi.lastIndexOf( ' ', foundPosition )
+
+ const wordEndPosition = baseGurmukhi.indexOf( ' ', foundPosition + sanitisedQuery.length )
+ // If the match finishes in the last word, no space will be deteced, and wordEndPosition
+ // will be -1. In this case, we want to end at the last position in the line.
+ const matchEndPosition = wordEndPosition === -1 ? baseGurmukhi.length - 1 : wordEndPosition
+
+ // Grab the word indexes in gurmukhi
+ const [ wordMatchStart, wordMatchLength ] = [
+ gurmukhi.substring( 0, matchStartPosition ).trim().split( ' ' ).length - 1,
+ gurmukhi.substring( matchStartPosition, matchEndPosition ).trim().split( ' ' ).length,
+ ]
+
+ const words = baseTarget.split( ' ' )
+
+ return [
+ `${words.slice( 0, wordMatchStart ).join( ' ' )} `,
+ `${words.slice( wordMatchStart, wordMatchStart + wordMatchLength ).join( ' ' )} `,
+ `${words.slice( wordMatchStart + wordMatchLength ).join( ' ' )} `,
+ ]
+}
+
+const firstLetterMatches = query => ( { target, gurmukhi } ) => {
+ const baseGurmukhi = stripVishraams( gurmukhi )
+ const baseLine = stripVishraams( target )
+
+ const letters = toAscii( firstLetters( stripAccents( toUnicode( baseGurmukhi ) ) ) )
+ const words = baseLine.split( ' ' )
+
+ const startPosition = letters.search( stripAccents( query ) )
+ const endPosition = startPosition + query.length
+
+ return [
+ `${words.slice( 0, startPosition ).join( ' ' )} `,
+ `${words.slice( startPosition, endPosition ).join( ' ' )} `,
+ `${words.slice( endPosition ).join( ' ' )} `,
+ ]
+}
+
+const highlighters = {
+ [ SEARCH_TYPES.fullWord ]: fullWordMatches,
+ [ SEARCH_TYPES.firstLetter ]: firstLetterMatches,
+}
+
+/**
+ * Separates the line into words before the first match, the first match, and after the match.
+ * @param target The text to highlight.
+ * @param context Contains gurmukhi and other contextual information required by all highlighters.
+ * @param searchQuery The string inputted by the user.
+ * @param searchMode The type of search being performed, either first word or full word.
+ * @return An array of [ beforeMatch, match, afterMatch ],
+ * with `match` being the highlighted section.`.
+ */
+const getHighlighter = ( searchQuery, searchMode ) => context => target => {
+ if ( !target ) return [ '', '', '' ]
+
+ // Account for wildcard characters
+ const sanitizedQuery = searchQuery.replace( new RegExp( '_', 'g' ), '.' )
+
+ // Select the right higlighter
+ const highlight = highlighters[ searchMode ]
+
+ return highlight( sanitizedQuery )( { target, ...context } )
+}
+
+export default getHighlighter