88 useCallback ,
99 useMemo ,
1010 useRef ,
11- RefObject
11+ RefObject ,
12+ useEffect
1213} from 'react' ;
1314
1415import type { NavItem } from 'epubjs' ;
@@ -41,6 +42,8 @@ interface EPUBContextType {
4142 handleLocationChanged : ( location : string | number ) => void ;
4243 setRendition : ( rendition : Rendition ) => void ;
4344 isAudioCombining : boolean ;
45+ highlightPattern : ( text : string ) => void ;
46+ clearHighlights : ( ) => void ;
4447}
4548
4649const EPUBContext = createContext < EPUBContextType | undefined > ( undefined ) ;
@@ -139,6 +142,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
139142 ttsModel,
140143 ttsInstructions,
141144 smartSentenceSplitting,
145+ epubHighlightEnabled,
142146 } = useConfig ( ) ;
143147 // Current document state
144148 const [ currDocData , setCurrDocData ] = useState < ArrayBuffer > ( ) ;
@@ -154,6 +158,8 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
154158 const isEPUBSetOnce = useRef ( false ) ;
155159 // Should pause ref
156160 const shouldPauseRef = useRef ( true ) ;
161+ // Track current highlight CFI for removal
162+ const currentHighlightCfi = useRef < string | null > ( null ) ;
157163
158164 /**
159165 * Clears all current document state and stops any active TTS
@@ -307,7 +313,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
307313
308314 // Get TOC for chapter titles
309315 const chapters = tocRef . current || [ ] ;
310-
316+
311317 // If we have a bookId, check for existing chapters to determine which indices already exist
312318 const existingIndices = new Set < number > ( ) ;
313319 if ( bookId ) {
@@ -329,21 +335,21 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
329335 console . error ( 'Error checking existing chapters:' , error ) ;
330336 }
331337 }
332-
338+
333339 // Create a map of section hrefs to their chapter titles
334340 const sectionTitleMap = new Map < string , string > ( ) ;
335-
341+
336342 // First, loop through all chapters to create the mapping
337343 for ( const chapter of chapters ) {
338344 if ( ! chapter . href ) continue ;
339345 const chapterBaseHref = chapter . href . split ( '#' ) [ 0 ] ;
340346 const chapterTitle = chapter . label . trim ( ) ;
341-
347+
342348 // For each chapter, find all matching sections
343349 for ( const section of sections ) {
344350 const sectionHref = section . href ;
345351 const sectionBaseHref = sectionHref . split ( '#' ) [ 0 ] ;
346-
352+
347353 // If this section matches this chapter, map it
348354 if ( sectionHref === chapter . href || sectionBaseHref === chapterBaseHref ) {
349355 sectionTitleMap . set ( sectionHref , chapterTitle ) ;
@@ -426,7 +432,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
426432
427433 // Get the chapter title from our pre-computed map
428434 let chapterTitle = sectionTitleMap . get ( section . href ) ;
429-
435+
430436 // If no chapter title found, use index-based naming
431437 if ( ! chapterTitle ) {
432438 chapterTitle = `Chapter ${ i + 1 } ` ;
@@ -466,7 +472,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
466472 }
467473
468474 const { bookId : returnedBookId , chapterIndex, duration } = await convertResponse . json ( ) ;
469-
475+
470476 if ( ! bookId ) {
471477 bookId = returnedBookId ;
472478 }
@@ -495,7 +501,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
495501 throw new Error ( 'Audiobook generation cancelled' ) ;
496502 }
497503 console . error ( 'Error processing section:' , error ) ;
498-
504+
499505 // Notify about error
500506 if ( onChapterComplete ) {
501507 onChapterComplete ( {
@@ -537,24 +543,24 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
537543
538544 const section = sections [ chapterIndex ] ;
539545 const trimmedText = section . text . trim ( ) ;
540-
546+
541547 if ( ! trimmedText ) {
542548 throw new Error ( 'No text content found in chapter' ) ;
543549 }
544550
545551 // Get TOC for chapter title
546552 const chapters = tocRef . current || [ ] ;
547553 const sectionTitleMap = new Map < string , string > ( ) ;
548-
554+
549555 for ( const chapter of chapters ) {
550556 if ( ! chapter . href ) continue ;
551557 const chapterBaseHref = chapter . href . split ( '#' ) [ 0 ] ;
552558 const chapterTitle = chapter . label . trim ( ) ;
553-
559+
554560 for ( const sect of sections ) {
555561 const sectionHref = sect . href ;
556562 const sectionBaseHref = sectionHref . split ( '#' ) [ 0 ] ;
557-
563+
558564 if ( sectionHref === chapter . href || sectionBaseHref === chapterBaseHref ) {
559565 sectionTitleMap . set ( sectionHref , chapterTitle ) ;
560566 }
@@ -705,6 +711,69 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
705711 }
706712 } , [ id , skipToLocation , extractPageText , setIsEPUB ] ) ;
707713
714+ const clearHighlights = useCallback ( ( ) => {
715+ if ( renditionRef . current ) {
716+ if ( currentHighlightCfi . current ) {
717+ renditionRef . current . annotations . remove ( currentHighlightCfi . current , 'highlight' ) ;
718+ currentHighlightCfi . current = null ;
719+ }
720+ }
721+ } , [ ] ) ;
722+
723+ const highlightPattern = useCallback ( async ( text : string ) => {
724+ if ( ! renditionRef . current ) return ;
725+
726+ // Clear existing highlights first
727+ clearHighlights ( ) ;
728+
729+ if ( ! epubHighlightEnabled ) return ;
730+
731+ if ( ! text || ! text . trim ( ) ) return ;
732+
733+ try {
734+ const contents = renditionRef . current . getContents ( ) ;
735+ const contentsArray = Array . isArray ( contents ) ? contents : [ contents ] ;
736+ for ( const content of contentsArray ) {
737+ const win = content . window ;
738+ if ( win && win . find ) {
739+ // Reset selection to start of document to ensure full search
740+ const sel = win . getSelection ( ) ;
741+ sel ?. removeAllRanges ( ) ;
742+
743+ // Attempt to find the text
744+ // window.find(aString, aCaseSensitive, aBackwards, aWrapAround, aWholeWord, aSearchInFrames, aShowDialog);
745+ // Note: We search for the trimmed text.
746+ if ( win . find ( text . trim ( ) , false , false , true , false , false , false ) ) {
747+ const range = sel ?. getRangeAt ( 0 ) ;
748+ if ( range ) {
749+ const cfi = content . cfiFromRange ( range ) ;
750+ // Store CFI for removal
751+ currentHighlightCfi . current = cfi ;
752+ renditionRef . current . annotations . add ( 'highlight' , cfi , { } , ( e : MouseEvent ) => {
753+ console . log ( 'Highlight clicked' , e ) ;
754+ } , '' , { fill : 'grey' , 'fill-opacity' : '0.4' , 'mix-blend-mode' : 'multiply' } ) ;
755+
756+ // Clear the browser selection so it doesn't look like user selected text
757+ sel ?. removeAllRanges ( ) ;
758+ return ; // Stop after first match
759+ }
760+ }
761+ }
762+ }
763+ } catch ( error ) {
764+ console . error ( 'Error highlighting text:' , error ) ;
765+ }
766+ } , [ clearHighlights , epubHighlightEnabled ] ) ;
767+
768+ // Effect to clear highlights when disabled
769+ useEffect ( ( ) => {
770+ if ( ! epubHighlightEnabled ) {
771+ clearHighlights ( ) ;
772+ }
773+ } , [ epubHighlightEnabled , clearHighlights ] ) ;
774+
775+
776+
708777 // Context value memoization
709778 const contextValue = useMemo (
710779 ( ) => ( {
@@ -725,6 +794,8 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
725794 handleLocationChanged,
726795 setRendition,
727796 isAudioCombining,
797+ highlightPattern,
798+ clearHighlights,
728799 } ) ,
729800 [
730801 setCurrentDocument ,
@@ -740,6 +811,8 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
740811 handleLocationChanged ,
741812 setRendition ,
742813 isAudioCombining ,
814+ highlightPattern ,
815+ clearHighlights ,
743816 ]
744817 ) ;
745818
0 commit comments