11import * as React from 'react'
2- import { MatchInfo , findTextMatch , NO_MATCH , NormalizerOptions } from './text-matching'
3- import { FC } from 'react'
2+ import { findTextMatches , NormalizerOptions , PositiveMatchInfo } from './text-matching'
3+ import { FC , ReactNode } from 'react'
44import { SxProps } from '@mui/material/styles'
55import Box from '@mui/material/Box'
66
@@ -35,6 +35,10 @@ const defaultHighlight: HighlightOptions = {
3535 sx : defaultHighlightStyle ,
3636}
3737
38+ export type HighlightPattern = string [ ]
39+
40+ export const NoHighlights : HighlightPattern = [ ]
41+
3842interface HighlightedTextProps {
3943 /**
4044 * The text to display
@@ -44,15 +48,15 @@ interface HighlightedTextProps {
4448 /**
4549 * The pattern to search for (and highlight)
4650 */
47- pattern : string | undefined
51+ pattern ?: HighlightPattern
4852
4953 /**
5054 * Instructions about which part to highlight.
5155 *
5256 * If not given, we will just search for the pattern.
5357 * If given, this will be executed, and the pattern will not even be considered.
5458 */
55- part ?: MatchInfo
59+ partsToHighlight ?: PositiveMatchInfo [ ]
5660
5761 /**
5862 * Options for highlighting (case sensitivity, styling, etc.)
@@ -67,29 +71,51 @@ interface HighlightedTextProps {
6771 */
6872export const HighlightedText : FC < HighlightedTextProps > = ( {
6973 text,
70- pattern,
71- part ,
74+ pattern = NoHighlights ,
75+ partsToHighlight ,
7276 options = defaultHighlight ,
7377} ) => {
7478 const { sx = defaultHighlightStyle , findOptions = { } } = options
7579
7680 // Have we been told what to highlight exactly? If not, look for the pattern
77- const task = part ?? findTextMatch ( text , [ pattern ] , findOptions )
81+ const matches = partsToHighlight ?? findTextMatches ( text , pattern , findOptions )
7882
7983 if ( text === undefined ) return undefined // Nothing to display
80- if ( task === NO_MATCH ) return < span > { text } </ span > // We don't have to highlight anything
81-
82- const beginning = text . substring ( 0 , task . startPos )
83- const focus = text . substring ( task . startPos , task . endPos )
84- const end = text . substring ( task . endPos )
85-
86- return (
87- < span style = { { textWrap : 'nowrap' } } >
88- { beginning }
89- < Box component = "mark" sx = { sx } >
90- { focus }
91- </ Box >
92- { end }
93- </ span >
84+ if ( ! matches . length ) return text // We don't have to highlight anything
85+
86+ const sortedMatches = matches . sort ( ( a , b ) =>
87+ a . startPos > b . startPos ? 1 : a . startPos < b . startPos ? - 1 : 0 ,
9488 )
89+
90+ const pieces : ReactNode [ ] = [ ]
91+ let processedChars = 0
92+ let processedMatches = 0
93+
94+ while ( processedChars < text . length ) {
95+ // Do we still have matches to highlight?
96+ if ( processedMatches < sortedMatches . length ) {
97+ // Yes, there are more matches
98+ const match = sortedMatches [ processedMatches ]
99+ if ( match . startPos < processedChars ) {
100+ // This match would collude with something already highlighted
101+ processedMatches ++ // just skip this match
102+ } else {
103+ // We want to highlight this match
104+ pieces . push ( text . substring ( processedChars , match . startPos ) )
105+ const focus = text . substring ( match . startPos , match . endPos )
106+ pieces . push (
107+ < Box key = { processedMatches } component = "mark" sx = { sx } >
108+ { focus }
109+ </ Box > ,
110+ )
111+ processedChars = match . endPos
112+ }
113+ } else {
114+ // No more matches, just grab the remaining string
115+ pieces . push ( text . substring ( processedChars ) )
116+ processedChars = text . length
117+ }
118+ }
119+
120+ return < span style = { { textWrap : 'nowrap' } } > { pieces } </ span >
95121}
0 commit comments