1- import { useMemo , useCallback } from 'react'
2- import type { CSSProperties } from 'react'
1+ import { useMemo , useCallback , createElement , cloneElement , Fragment , isValidElement } from 'react'
2+ import type { CSSProperties , ReactNode } from 'react'
33
44import { useTts } from './hook.js'
5- import type { TTSHookProps } from './hook.js'
5+ import type { TTSHookProps , TTSRenderProp } from './hook.js'
6+ import type { TTSBoundaryUpdate } from './controller.js'
7+ import { isStringOrNumber , stripPunctuation , extractTextFromChildren } from './utils.js'
8+ import { Highlighter } from './highlighter.js'
69import { iconSizes , Sizes } from './icons.js'
710import type { SvgProps } from './icons.js'
811import { Control , padding as ctrlPadding } from './control.js'
@@ -103,6 +106,90 @@ const content = (): CSSProperties => {
103106 gridArea : 'cnt'
104107 }
105108}
109+
110+ /**
111+ * Default render prop implementation that handles complex React children
112+ * by recursively applying highlighting similar to the old Children.map approach
113+ */
114+ const createDefaultRenderer = (
115+ text : string ,
116+ markColor ?: string ,
117+ markBackgroundColor ?: string
118+ ) : TTSRenderProp => {
119+ let textBuffer = ''
120+
121+ const parseChildrenRecursively = (
122+ children : ReactNode ,
123+ boundary : TTSBoundaryUpdate ,
124+ markTextAsSpoken : boolean
125+ ) : ReactNode => {
126+ if ( ! children ) return children
127+
128+ // Handle arrays of children
129+ if ( Array . isArray ( children ) ) {
130+ return children . map ( ( child : ReactNode ) =>
131+ parseChildrenRecursively ( child , boundary , markTextAsSpoken )
132+ )
133+ }
134+
135+ // Handle React elements
136+ if ( isValidElement ( children ) ) {
137+ const childProps = children . props && typeof children . props === 'object' ? children . props : { }
138+
139+ return cloneElement ( children , {
140+ ...childProps ,
141+ children : parseChildrenRecursively (
142+ 'children' in childProps ? ( childProps . children as ReactNode ) : undefined ,
143+ boundary ,
144+ markTextAsSpoken
145+ )
146+ } as Record < string , unknown > )
147+ }
148+
149+ // Handle string and number primitives
150+ if ( isStringOrNumber ( children ) ) {
151+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
152+ const childText = String ( children )
153+ const { word, startChar, endChar } = boundary
154+ const bufferTextLength = textBuffer . length
155+
156+ textBuffer += `${ childText } `
157+
158+ if ( markTextAsSpoken && word ) {
159+ const start = startChar - bufferTextLength
160+ const end = endChar - bufferTextLength
161+ const prev = childText . substring ( 0 , start )
162+ const found = childText . substring ( start , end )
163+ const after = childText . substring ( end , childText . length )
164+
165+ if ( found && stripPunctuation ( found ) === stripPunctuation ( word ) ) {
166+ const Highlight = createElement ( Highlighter , {
167+ text : found ,
168+ mark : stripPunctuation ( found ) ,
169+ color : markColor ,
170+ backgroundColor : markBackgroundColor
171+ } )
172+
173+ return createElement (
174+ Fragment ,
175+ { key : `tts-${ start } -${ end } ` } ,
176+ prev ,
177+ Highlight ,
178+ after
179+ )
180+ }
181+ }
182+ }
183+
184+ return children
185+ }
186+
187+ return ( { children, boundary, markTextAsSpoken } ) => {
188+ // Reset the buffer for each render
189+ textBuffer = ''
190+ return parseChildrenRecursively ( children , boundary , markTextAsSpoken )
191+ }
192+ }
106193/**
107194 * `useTts` is a React hook for converting text to speech using
108195 * the `SpeechSynthesis` and `SpeechSynthesisUtterance` Browser API's.
@@ -128,6 +215,8 @@ const TextToSpeech = ({
128215 voice,
129216 volume,
130217 children,
218+ text,
219+ render,
131220 position,
132221 onStart,
133222 onPause,
@@ -148,12 +237,24 @@ const TextToSpeech = ({
148237 markTextAsSpoken = false ,
149238 useStopOverPause = false
150239} : TTSProps ) => {
240+ // Create a default renderer for complex content when markTextAsSpoken is true
241+ // but no custom render prop is provided
242+ const defaultRenderer = useMemo ( ( ) => {
243+ if ( markTextAsSpoken && ! render ) {
244+ const extractedText = text || ( children ? extractTextFromChildren ( children ) : '' )
245+ return createDefaultRenderer ( extractedText , markColor , markBackgroundColor )
246+ }
247+ return undefined
248+ } , [ markTextAsSpoken , render , text , children , markColor , markBackgroundColor ] )
249+
151250 const { state, replay, toggleMute, playOrPause, playOrStop, ttsChildren } = useTts ( {
152251 lang,
153252 rate,
154253 voice,
155254 volume,
156255 children,
256+ text,
257+ render : render || defaultRenderer ,
157258 onStart,
158259 onPause,
159260 onBoundary,
0 commit comments