@@ -76,47 +76,67 @@ const GlossaryInjector: React.FC<GlossaryInjectorProps> = ({ children }) => {
7676 const newNodes : Node [ ] = [ ] ;
7777 let hasReplacements = false ;
7878
79- // Create a regex pattern to match all terms as whole words (case-insensitive) .
79+ // Create a regex pattern to match both exact terms and their plural forms .
8080 const regexPattern = terms . map ( term => {
8181 const escapedTerm = term . replace ( / [ - \/ \\ ^ $ * + ? . ( ) | [ \] { } ] / g, '\\$&' ) ;
82- // Match exact terms at word boundaries .
83- return `(\\b${ escapedTerm } \\b)` ;
82+ // Match exact term or term followed by 's' or 'es' at word boundary .
83+ return `(\\b${ escapedTerm } (s|es)? \\b)` ;
8484 } ) . join ( '|' ) ;
8585 const regex = new RegExp ( regexPattern , 'gi' ) ; // The 'i' flag is for case-insensitive matching.
8686
8787 let lastIndex = 0 ;
8888 let match : RegExpExecArray | null ;
8989
9090 while ( ( match = regex . exec ( currentText ) ) ) {
91- const matchedText = match [ 0 ] ; // The actual text as it appears in the content.
92- const matchedTerm = terms . find ( term =>
93- term . toLowerCase ( ) === matchedText . toLowerCase ( )
94- ) || matchedText ; // Find the canonical term in the glossary.
91+ const matchedText = match [ 0 ] ; // The full matched text (may include plural suffix).
92+
93+ // Find the base term from the glossary that matches (without plural).
94+ const baseTerm = terms . find ( term =>
95+ matchedText . toLowerCase ( ) === term . toLowerCase ( ) ||
96+ matchedText . toLowerCase ( ) === `${ term . toLowerCase ( ) } s` ||
97+ matchedText . toLowerCase ( ) === `${ term . toLowerCase ( ) } es`
98+ ) ;
99+
100+ if ( ! baseTerm ) {
101+ // Skip if no matching base term found.
102+ continue ;
103+ }
95104
96105 if ( lastIndex < match . index ) {
97106 newNodes . push ( document . createTextNode ( currentText . slice ( lastIndex , match . index ) ) ) ;
98107 }
99108
100- const isFirstMention = ! processedTerms . has ( matchedTerm . toLowerCase ( ) ) ;
109+ const isFirstMention = ! processedTerms . has ( baseTerm . toLowerCase ( ) ) ;
101110 const isLink = parentElement && parentElement . tagName === 'A' ; // Check if the parent is a link.
102111
103112 if ( isFirstMention && ! isLink ) {
104113 // Create a tooltip only if it's the first mention and not a link.
105114 const tooltipWrapper = document . createElement ( 'span' ) ;
106- tooltipWrapper . setAttribute ( 'data-term' , matchedTerm ) ;
115+ tooltipWrapper . setAttribute ( 'data-term' , baseTerm ) ;
107116 tooltipWrapper . className = 'glossary-term' ;
108117
109- const definition = glossary [ matchedTerm ] ; // Get definition using the canonical term.
118+ const definition = glossary [ baseTerm ] ;
119+
120+ // Extract the part to underline (the base term) and the suffix (if plural).
121+ let textToUnderline = matchedText ;
122+ let suffix = '' ;
123+
124+ if ( matchedText . toLowerCase ( ) !== baseTerm . toLowerCase ( ) ) {
125+ // This is a plural form - only underline the base part.
126+ const baseTermLength = baseTerm . length ;
127+ textToUnderline = matchedText . substring ( 0 , baseTermLength ) ;
128+ suffix = matchedText . substring ( baseTermLength ) ;
129+ }
110130
111131 ReactDOM . render (
112- < GlossaryTooltip term = { matchedTerm } definition = { definition } >
113- { matchedText } { /* No space after the term. */ }
132+ < GlossaryTooltip term = { baseTerm } definition = { definition } >
133+ { textToUnderline } { suffix && < span className = "no-underline" > { suffix } </ span > }
114134 </ GlossaryTooltip > ,
115135 tooltipWrapper
116136 ) ;
117137
118138 newNodes . push ( tooltipWrapper ) ;
119- processedTerms . add ( matchedTerm . toLowerCase ( ) ) ; // Mark this term as processed (case-insensitive).
139+ processedTerms . add ( baseTerm . toLowerCase ( ) ) ;
120140 } else if ( isLink ) {
121141 // If it's a link, we skip this mention but do not mark it as processed.
122142 newNodes . push ( document . createTextNode ( matchedText ) ) ;
0 commit comments