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'
3
3
4
4
import { 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'
6
9
import { iconSizes , Sizes } from './icons.js'
7
10
import type { SvgProps } from './icons.js'
8
11
import { Control , padding as ctrlPadding } from './control.js'
@@ -103,6 +106,90 @@ const content = (): CSSProperties => {
103
106
gridArea : 'cnt'
104
107
}
105
108
}
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
+ }
106
193
/**
107
194
* `useTts` is a React hook for converting text to speech using
108
195
* the `SpeechSynthesis` and `SpeechSynthesisUtterance` Browser API's.
@@ -128,6 +215,8 @@ const TextToSpeech = ({
128
215
voice,
129
216
volume,
130
217
children,
218
+ text,
219
+ render,
131
220
position,
132
221
onStart,
133
222
onPause,
@@ -148,12 +237,24 @@ const TextToSpeech = ({
148
237
markTextAsSpoken = false ,
149
238
useStopOverPause = false
150
239
} : 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
+
151
250
const { state, replay, toggleMute, playOrPause, playOrStop, ttsChildren } = useTts ( {
152
251
lang,
153
252
rate,
154
253
voice,
155
254
volume,
156
255
children,
256
+ text,
257
+ render : render || defaultRenderer ,
157
258
onStart,
158
259
onPause,
159
260
onBoundary,
0 commit comments