UNPKG

react-text-to-speech

Version:

An easy-to-use React.js library that leverages the Web Speech API to convert text to speech.

789 lines (788 loc) 27.7 kB
import { HiMiniStop, HiVolumeOff, HiVolumeUp } from "./icons.mjs"; import React, { Fragment, cloneElement, forwardRef, isValidElement, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; const punctuationDelimiters = [ ".", "?", "!" ]; const chunkDelimiters = [ "\n", ...punctuationDelimiters.map((delimiter) => delimiter + " "), " " ]; const defaults = { pitch: 1, rate: 1, volume: 1, lang: "", voice: "", props: {} }; const desktopChunkSize = 1e3; const directiveRegex = /\[\[(\w+)=([^\]=]+)\]\] ?/; const directiveRegexGlobal = new RegExp(directiveRegex.source, "g"); const highlightedTextIdSuffix = "-highlighted-text"; const idPrefix = "rtts-"; const iosRegex = /iPhone|iPad|iPod/i; const lineSplitRegex = /(.*?)(\n)(.*)/; const mobileRegex = /Android|webOS|BlackBerry|IEMobile|Opera Mini/i; const nonWhitespaceRegex = /\S/; const symbolMapping = { "<": "lessthan", ">": "greaterthan" }; const sanitizeRegex = new RegExp(`[${Object.keys(symbolMapping).join("")}]|(&[^\s;]+);`, "g"); const sentenceDelimiters = ["\n", ...punctuationDelimiters]; const sentenceSplitRegex = /(.*?)(\n|[.!?]\s)(.*)/; const trailingSpacesRegex = /[ \t]+$/; //#endregion //#region src/lib/dom.ts const composeClass = (baseClass, props) => `${baseClass}${props?.className ? " " + props.className : ""}`; const composeProps = (baseClass, props) => ({ ...props, className: composeClass(baseClass, props) }); function createElementWithProps(tag, props = {}) { const element = document.createElement(tag); Object.keys(props).forEach((key) => { const value = props[key]; if (value == null) return; if (key === "style" && typeof value === "object") Object.assign(element.style, value); else element[key] = value; }); return element; } function hideElement(element) { if (!element.dataset["originalDisplay"]) element.dataset["originalDisplay"] = element.style.display || getComputedStyle(element).display || "block"; Object.assign(element.style, { display: "none" }); } function showElement(element) { const display = element.dataset["originalDisplay"]; if (display) Object.assign(element.style, { display }); } //#endregion //#region src/lib/state.ts const state = { stopReason: "manual" }; function setState(newState) { Object.assign(state, newState); } //#endregion //#region src/lib/utils.ts function cancel(stopReason = "manual") { if (typeof window === "undefined") return; setState({ stopReason }); window.speechSynthesis?.cancel(); } function chunkBySizeWithDelimiters(text, size) { const length = text.length; const result = []; let startIndex = 0; while (startIndex < length) { let endIndex = Math.min(startIndex + size, length); if (endIndex < length && text[endIndex] !== "\n") for (const delimiter of chunkDelimiters) { const delimiterIndex = text.lastIndexOf(delimiter, endIndex) + delimiter.length - 1; if (delimiterIndex > startIndex) { endIndex = delimiterIndex; break; } } result.push(text.slice(startIndex, endIndex)); startIndex = endIndex; } return result; } const getProgress = (current, total) => total && Math.floor(current / total * 100); function isMobile(iOS = true) { let result = navigator.userAgentData?.mobile; result ??= mobileRegex.test(navigator.userAgent) || iOS && iosRegex.test(navigator.userAgent); return result; } function parse(value) { if (value === "true") return true; if (value === "false") return false; const number = +value; if (!isNaN(number) && value !== "") return number; return value; } const sanitize = (text) => text.replace(sanitizeRegex, (match, group) => group ? group + ")" : ` ${symbolMapping[match]} `); function shouldHighlightNextPart(highlightMode, utterance, charIndex) { if (highlightMode === "word" || !charIndex) return true; const text = utterance.text.slice(0, charIndex).replace(trailingSpacesRegex, " ").slice(-2); if (highlightMode === "sentence" && (text[1] === "\n" || text[0] && sentenceDelimiters.includes(text[0]) && text[1] === " ")) return true; if (highlightMode === "line" && (text[1] === "\n" || text[0] === "\n" && text[1] === " ")) return true; return false; } function splitNode(highlightMode, node, speakingWord) { if (highlightMode === "paragraph") return [ "", node, "" ]; const { index, length } = speakingWord; const beforeIndex = +index.split("-").at(-1); const before = node.slice(0, beforeIndex); if (highlightMode === "word") return [ before, node.slice(beforeIndex, beforeIndex + length), node.slice(beforeIndex + length) ]; node = node.slice(beforeIndex); const match = node.match(highlightMode === "sentence" ? sentenceSplitRegex : lineSplitRegex); if (!match) return [ before, node, "" ]; const sentence = match[1] + (match[2] ?? "").trimEnd(); return [ before, sentence, node.slice(sentence.length) ]; } function textToChunks(text, size, enableDirectives) { size = size ? Math.max(size, 50) : isMobile() ? 250 : desktopChunkSize; const regex = new RegExp(`${enableDirectives ? directiveRegex.source + "|" : ""}​`, "g"); const chunks = []; let currentIndex = 0; let match; while ((match = regex.exec(text)) !== null) { const directiveIndex = match.index; if (directiveIndex > currentIndex) { const preDirectiveText = text.slice(currentIndex, directiveIndex); chunks.push(...chunkBySizeWithDelimiters(preDirectiveText, size)); } if (match[0] !== "​") chunks.push(match[0]); currentIndex = regex.lastIndex; } if (currentIndex < text.length) { const remainingText = text.slice(currentIndex); chunks.push(...chunkBySizeWithDelimiters(remainingText, size)); } return chunks; } //#endregion //#region src/lib/queue.ts const queue = []; const queueListeners = []; function addToQueue(item, callback) { queue.push(item); emit(callback); } function clearQueue(cancelSpeech = false, start = 0, emitEvent = false) { if (cancelSpeech) cancel(); queue.slice(start).forEach(({ setSpeechStatus }) => setSpeechStatus("stopped")); queue.length = 0; if (emitEvent) emit(); } const clearQueueHook = () => clearQueue(true, 1, true); const clearQueueUnload = () => clearQueue(true, 1); function dequeue(index = 0) { if (index === 0) cancel(); else removeFromQueue(index); } function emit(callback) { const utteranceQueue = queue.map(({ text, utterance: { pitch, rate, volume, lang, voice } }) => ({ text, pitch, rate, volume, lang, voice })); queueListeners.forEach((listener) => listener(utteranceQueue)); callback?.(utteranceQueue); } function removeFromQueue(utterance, callback) { const index = typeof utterance === "number" ? utterance : queue.findIndex((item) => item.utterance === utterance); if (index === -1) return; const [item] = queue.splice(index, 1); if (item) { if (index === 0) cancel(); else item.setSpeechStatus("stopped"); emit(callback); } } function speakFromQueue() { const item = queue[0]; if (item) window.speechSynthesis.speak(item.utterance); } function subscribe(callback) { queueListeners.push(callback); return () => { const index = queueListeners.indexOf(callback); if (index !== -1) queueListeners.splice(index, 1); }; } //#endregion //#region src/lib/react.tsx function findCharIndex(words, index) { let currentIndex = 0; function recursiveSearch(currentWords, parentIndex = "") { if (typeof currentWords === "string") { const elementIndex = index - currentIndex; return (currentIndex += currentWords.length) > index ? getIndex(parentIndex, elementIndex) : ""; } for (let i = 0; i < currentWords.length; i++) { const result = recursiveSearch(currentWords[i], i); if (result) return getIndex(parentIndex, result); } return ""; } return recursiveSearch(words); } const getIndex = (parentIndex, index) => `${parentIndex === "" ? "" : parentIndex + "-"}${index}`; function indexText(node, id, index = "") { if (typeof node == "string" || typeof node == "number") return node; if (Array.isArray(node)) return node.map((child, i) => indexText(child, id, getIndex(index, i))); if (isValidElement(node)) { const props = { key: node.key ?? index, children: indexText(normalizeChildren(node.props.children), id, index) }; if (node.type !== Fragment) props.className = composeClass(`${id}${index}`, node.props); return cloneElement(node, props); } return node; } function isParent(parentIndex, index) { if (!index?.startsWith(parentIndex)) return false; if (parentIndex) { const indexParts = index.split("-"); const parentIndexParts = parentIndex.split("-"); for (let i = 0; i < parentIndexParts.length; i++) if (indexParts[i] !== parentIndexParts[i]) return false; } return true; } function isSetStateFunction(v) { return typeof v === "function"; } function nodeToKey(node) { if (typeof node === "string") return node; if (typeof node === "number") return String(node); if (Array.isArray(node)) return node.map(nodeToKey).join(""); if (isValidElement(node)) { const nodeType = typeof node.type === "string" ? node.type : "Component"; const { children, ...props } = node.props; return `${nodeType}(${JSON.stringify(props)})[${nodeToKey(children)}]`; } return ""; } function nodeToWords(node) { if (typeof node === "string") return node; if (typeof node === "number") return String(node); if (Array.isArray(node)) return node.map(nodeToWords); if (isValidElement(node)) return nodeToWords(normalizeChildren(node.props.children)); return ""; } function normalizeChildren(node) { if (Array.isArray(node)) return node.flat().map((element, i) => { if (typeof element === "number" || typeof element === "string" && nonWhitespaceRegex.test(element)) return /* @__PURE__ */ React.createElement("span", { key: i }, element); return element; }); if (isValidElement(node)) return [node]; return node; } function parent(index) { if (!index) return ""; const lastIndex = index.lastIndexOf("-"); return lastIndex === -1 ? "" : index.slice(0, lastIndex); } function stripDirectives(node) { if (typeof node === "string") return node.replace(directiveRegexGlobal, ""); if (typeof node === "number") return node; if (Array.isArray(node)) return node.map(stripDirectives); if (isValidElement(node)) return cloneElement(node, { children: stripDirectives(node.props.children) }); return null; } function toText(node) { if (typeof node === "string") return node; if (typeof node === "number") return String(node); if (Array.isArray(node)) return node.map(toText).join("​"); if (isValidElement(node)) return toText(node.props.children); return ""; } //#endregion //#region src/hooks.tsx function useStableValue(value, mode, delay) { const [stableValue, setStableValue] = useState(value); const lastUpdated = useRef(0); useEffect(() => { let timeout = null; if (mode === "immediate" || delay <= 0) timeout = setTimeout(() => setStableValue(value), 0); else if (mode === "debounce") timeout = setTimeout(() => setStableValue(value), delay); else if (mode === "throttle") { const elapsed = Date.now() - lastUpdated.current; timeout = setTimeout(() => { setStableValue(value); lastUpdated.current = Date.now(); }, Math.max(0, delay - elapsed)); } return () => { if (timeout) clearTimeout(timeout); }; }, [ value, mode, delay ]); return stableValue; } function useQueue() { const [queue, setQueue] = useState([]); useEffect(() => subscribe(setQueue), []); return { queue, dequeue, clearQueue: clearQueueHook }; } function useSpeak(options) { const [speechProps, setSpeechProps] = useState({ text: "" }); const { start, ...speechInterface } = useSpeech({ ...speechProps, ...options, stableText: true, autoPlay: false }); const speak = useCallback((text, options = {}) => setSpeechProps({ text, ...options }), []); useEffect(() => { if (speechProps.text) start(); }, [speechProps]); return { speak, start, ...speechInterface }; } function useSpeech(speechProps) { const { uniqueId, normalizedText, reactContent, ...speechInterface } = useSpeechInternal(speechProps); return speechInterface; } /** @internal */ function useSpeechInternal({ id, text, stableText = false, pitch = defaults.pitch, rate = defaults.rate, volume = defaults.volume, lang = defaults.lang, voiceURI = defaults.voice, autoPlay = false, preserveUtteranceQueue = false, highlightText = false, showOnlyHighlightedText = false, highlightMode = "word", highlightProps, highlightContainerProps, enableDirectives = false, updateMode = "immediate", updateDelay = 0, maxChunkSize, onError = console.error, onStart, onResume, onPause, onStop, onBoundary, onQueueChange }) { const [speechStatus, speechStatusRef, setSpeechStatus] = useStateRef("stopped"); const [speakingWord, speakingWordRef, setSpeakingWord] = useStateRef(null); const { utteranceRef, updateProps } = useSpeechSynthesisUtterance(); const highlightRef = useRef(false); const directiveRef = useRef({ event: null, delay: 0 }); const uniqueId = useMemo(() => `${idPrefix}${id ?? crypto.randomUUID()}`, [id]); const key = useMemo(() => stableText ? text : nodeToKey(text), [text, stableText]); const stableKey = useStableValue(key, updateMode, updateDelay); const stringifiedVoices = useMemo(() => voiceURI.toString(), [voiceURI]); const normalizedText = useMemo(() => isValidElement(text) ? [text] : text, [stableKey]); const { indexedText, sanitizedText, speechText, words } = useMemo(() => { const strippedText = enableDirectives ? stripDirectives(normalizedText) : normalizedText; const words = nodeToWords(strippedText); const sanitizedText = ` ${sanitize(toText(enableDirectives ? normalizedText : words))}`; const speechText = stripDirectives(sanitizedText).trimStart(); return { indexedText: indexText(strippedText, uniqueId), sanitizedText, speechText, words }; }, [enableDirectives, normalizedText]); const chunks = useMemo(() => textToChunks(sanitizedText, maxChunkSize, enableDirectives), [ enableDirectives, maxChunkSize, sanitizedText ]); const reactContent = useMemo(() => showOnlyHighlightedText ? highlightedText(indexedText) : indexedText, [ speakingWord, showOnlyHighlightedText, highlightMode, indexedText ]); const Text = useCallback(forwardRef((props, ref) => /* @__PURE__ */ React.createElement("div", { ref, ...composeProps(uniqueId, props) }, reactContent)), [reactContent]); function reset(event = null) { directiveRef.current.abortDelay?.(); directiveRef.current = { event, delay: 0 }; } function resumeEventHandler() { setSpeechStatus("started"); onResume?.(); } function pauseEventHandler() { setSpeechStatus("paused"); onPause?.(); } function start() { const synth = window.speechSynthesis; if (!synth) return onError(/* @__PURE__ */ new Error("Browser not supported! Try some other browser.")); if (speechStatusRef.current === "paused") { if (directiveRef.current.event === "pause") speakFromQueue(); synth.resume(); return resumeEventHandler(); } if (speechStatusRef.current === "queued") return; let currentChunk = 0; let currentText = chunks[currentChunk] || ""; let processedTextLength = -1; const utterance = utteranceRef.current; utterance.text = currentText.trimStart(); let offset = processedTextLength + currentText.length - utterance.text.length; let specialSymbolOffset = 0; updateProps({ pitch, rate, volume, lang, voiceURI }); function handleDirectives() { let skip = false; while (currentChunk < chunks.length) { const match = directiveRegex.exec(currentText); if (!match) { if (!skip) return true; processedTextLength += currentText.length; } else { const key = match[1]; const value = parse(match[2] ?? ""); switch (key) { case "delay": directiveRef.current.delay += value; break; case "pitch": case "rate": case "volume": case "lang": case "voice": updateProps({ [key]: value === "default" ? defaults[key] : value }); break; case "skip": if (typeof value === "boolean") skip = value; break; } } currentText = chunks[++currentChunk] ?? ""; } return false; } function startEventHandler() { window.addEventListener("beforeunload", clearQueueUnload); setState({ stopReason: "auto" }); highlightRef.current = true; if (directiveRef.current.event || directiveRef.current.delay) reset(); else onStart?.(); onBoundary?.({ progress: getProgress(offset, speechText.length) }); } async function stopEventHandler() { if (state.stopReason === "auto" && currentChunk < chunks.length - 1) { processedTextLength += currentText.length; currentText = chunks[++currentChunk] ?? ""; if (!enableDirectives || handleDirectives()) { utterance.text = currentText.trimStart(); offset = processedTextLength + currentText.length - utterance.text.length; if (speechStatusRef.current === "paused") return reset("pause"); const { delay } = directiveRef.current; directiveRef.current.event = "change"; if (!delay) return speakFromQueue(); const timeout = setTimeout(speakFromQueue, delay); directiveRef.current.abortDelay = () => clearTimeout(timeout); return; } } if (state.stopReason === "change") { if (speakingWordRef.current) { const currentLength = utterance.text.length; utterance.text = utterance.text.slice(speakingWordRef.current.charIndex).trimStart(); offset += currentLength - utterance.text.length; setSpeakingWord(null); } return speakFromQueue(); } if (state.stopReason === "auto") onBoundary?.({ progress: 100 }); if (synth.paused) cancel(); window.removeEventListener("beforeunload", clearQueueUnload); setSpeechStatus("stopped"); setSpeakingWord(null); utterance.onstart = null; utterance.onend = null; utterance.onerror = null; utterance.onboundary = null; removeFromQueue(utterance, onQueueChange); speakFromQueue(); onStop?.(); } function boundaryEventHandler(event) { const { charIndex, charLength, name } = event; if (name === "word") { const isSpecialSymbol = +(utterance.text[charIndex + charLength] === "\xA0"); const index = findCharIndex(words, offset + specialSymbolOffset + charIndex - isSpecialSymbol); if (shouldHighlightNextPart(highlightMode, utterance, charIndex) || parent(index) !== parent(speakingWordRef.current?.index)) setSpeakingWord({ index, charIndex: isSpecialSymbol ? charIndex + charLength + 1 : charIndex, length: isSpecialSymbol || charLength }); if (isSpecialSymbol) specialSymbolOffset -= charLength + 1; } onBoundary?.({ progress: getProgress(offset + charIndex + charLength, speechText.length) }); } utterance.onstart = startEventHandler; utterance.onend = stopEventHandler; utterance.onerror = stopEventHandler; utterance.onboundary = boundaryEventHandler; if (!preserveUtteranceQueue) clearQueue(); addToQueue({ text: speechText, utterance, setSpeechStatus }, onQueueChange); setSpeechStatus("started"); if (!synth.speaking) return speakFromQueue(); if (preserveUtteranceQueue && speechStatus !== "started") return setSpeechStatus("queued"); cancel(); } function pause() { if (isMobile(false) || speechStatusRef.current === "queued") return stop(); if (speechStatusRef.current === "started") { if (directiveRef.current.delay) reset("pause"); else window.speechSynthesis?.pause(); pauseEventHandler(); } } function stop({ status = speechStatusRef.current, stopReason } = {}) { if (status === "stopped") return; if (status === "queued") { removeFromQueue(utteranceRef.current, onQueueChange); return setSpeechStatus("stopped"); } if (directiveRef.current.delay || directiveRef.current.event === "pause") { reset(); speakFromQueue(); } cancel(stopReason); } function highlightedText(node, index = "") { if (!highlightText || !isParent(index, speakingWord?.index)) return; switch (typeof node) { case "number": node = String(node); case "string": const highlighted = splitNode(highlightMode, node, speakingWord)[1]; return /* @__PURE__ */ React.createElement("span", highlightContainerProps, /* @__PURE__ */ React.createElement("mark", highlightProps, highlighted)); } if (Array.isArray(node)) return node.map((child, i) => highlightedText(child, getIndex(index, i))); if (isValidElement(node)) return cloneElement(node, { children: highlightedText(node.props.children, index) }); } useEffect(() => { return () => stop({ status: speechStatusRef.current }); }, [enableDirectives, key]); useLayoutEffect(() => { highlightRef.current = false; if (autoPlay) start(); }, [ autoPlay, enableDirectives, normalizedText ]); useLayoutEffect(() => { if (!highlightText || !highlightRef.current || showOnlyHighlightedText || !speakingWord) return; const parentIndex = speakingWord.index.split("-").slice(0, -1).join("-"); const elementSnapshots = Array.from(document.getElementsByClassName(`${uniqueId}${parentIndex}`)).map((element) => { const originalTextContent = element.textContent; const [before, highlighted, after] = splitNode(highlightMode, element.textContent, speakingWord); const container = createElementWithProps("span", highlightContainerProps); const mark = createElementWithProps("mark", highlightProps); mark.textContent = highlighted; if (before) container.appendChild(document.createTextNode(before)); container.appendChild(mark); if (after) container.appendChild(document.createTextNode(after)); element.textContent = ""; element.appendChild(container); return [element, originalTextContent]; }); return () => { if (highlightRef.current) elementSnapshots.forEach(([element, originalTextContent]) => element.textContent = originalTextContent); }; }, [ speakingWord, highlightText, showOnlyHighlightedText, highlightMode ]); useEffect(() => { if (speechStatusRef.current !== "started") return; const timeout = setTimeout(() => { updateProps({ pitch, rate, volume, lang, voiceURI }); if (directiveRef.current.delay) return; stop({ stopReason: "change" }); emit(onQueueChange); }, 500); return () => clearTimeout(timeout); }, [ pitch, rate, volume, lang, stringifiedVoices ]); return { uniqueId, normalizedText, reactContent, Text, speechStatus, isInQueue: speechStatus === "started" || speechStatus === "queued", start, pause, stop: () => stop() }; } function useSpeechSynthesisUtterance() { const utteranceRef = useRef(typeof window === "undefined" || !window.speechSynthesis ? null : new SpeechSynthesisUtterance()); const { voices } = useVoices(); function updateProps({ pitch, rate, volume, lang, voiceURI }) { const utterance = utteranceRef.current; if (!utterance) return; if (pitch !== void 0) utterance.pitch = pitch; if (rate !== void 0) utterance.rate = rate; if (volume !== void 0) utterance.volume = volume; if (lang !== void 0) utterance.lang = lang; if (voiceURI === void 0) return; if (!voiceURI) { utterance.voice = null; return; } if (!Array.isArray(voiceURI)) voiceURI = [voiceURI]; for (let i = 0; i < voiceURI.length; i++) { const uri = voiceURI[i]; const voice = voices.find((voice) => voice.voiceURI === uri); if (voice) { utterance.voice = voice; if (!lang) utterance.lang = voice.lang; return; } } } return { utteranceRef, updateProps }; } function useStateRef(init) { const [state, setState] = useState(init); const ref = useRef(init); function setStateRef(value) { setState((prev) => { const next = isSetStateFunction(value) ? value(prev) : value; ref.current = next; return next; }); } return [ state, ref, setStateRef ]; } function useVoices() { const [languages, setLanguages] = useState([]); const [voices, setVoices] = useState([]); function setData(voices) { setLanguages(Array.from(new Set(voices.map(({ lang }) => lang)))); setVoices(voices); } useEffect(() => { const synth = window.speechSynthesis; if (!synth) return; const voices = synth.getVoices(); if (voices.length) setData(voices); else { const onVoicesChanged = () => setData(synth.getVoices()); synth.addEventListener("voiceschanged", onVoicesChanged); return () => synth.removeEventListener("voiceschanged", onVoicesChanged); } }, []); return { languages, voices }; } //#endregion //#region src/components.tsx function HighlightedText({ id, children, ...props }) { const uniqueId = `${idPrefix}${id}`; return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", composeProps(uniqueId, props)), /* @__PURE__ */ React.createElement("div", composeProps(`${uniqueId}${highlightedTextIdSuffix}`, props))); } function Speech({ startBtn = /* @__PURE__ */ React.createElement(HiVolumeUp, null), pauseBtn = /* @__PURE__ */ React.createElement(HiVolumeOff, null), stopBtn = /* @__PURE__ */ React.createElement(HiMiniStop, null), useStopOverPause = false, enableConditionalHighlight = false, props = defaults.props, children, ...hookProps }) { const { uniqueId, normalizedText, reactContent, Text, speechStatus, ...childrenOptions } = useSpeechInternal(hookProps); const { isInQueue, start, pause, stop } = childrenOptions; const sourceRef = useRef(null); useLayoutEffect(() => { const containers = Array.from(document.getElementsByClassName(uniqueId)).filter((container) => container !== sourceRef.current); if (!containers.length) return; const sourceHTML = sourceRef.current.innerHTML; containers.forEach((container) => container.innerHTML = sourceHTML); if (!enableConditionalHighlight) return; const observer = new MutationObserver(() => { Array.from(document.getElementsByClassName(uniqueId)).forEach((container) => { if (!container.innerHTML) container.innerHTML = sourceHTML; }); }); observer.observe(document.body, { childList: true, subtree: true }); return () => observer.disconnect(); }, [normalizedText, uniqueId]); useLayoutEffect(() => { const containers = Array.from(document.getElementsByClassName(uniqueId)).filter((container) => container !== sourceRef.current); if (!containers.length) return; const highlightedTextContainers = Array.from(document.getElementsByClassName(`${uniqueId}${highlightedTextIdSuffix}`)); if (hookProps.showOnlyHighlightedText) { const sourceHTML = sourceRef.current.innerHTML; containers.forEach(hideElement); highlightedTextContainers.forEach((container) => { container.innerHTML = sourceHTML; showElement(container); }); } else { highlightedTextContainers.forEach(hideElement); containers.forEach(showElement); } }, [reactContent, uniqueId]); return typeof children === "function" ? children(childrenOptions) : /* @__PURE__ */ React.createElement("div", { style: { display: "flex", columnGap: "1rem" }, ...props }, !isInQueue ? /* @__PURE__ */ React.createElement("span", { role: "button", onClick: start }, startBtn) : useStopOverPause ? /* @__PURE__ */ React.createElement("span", { role: "button", onClick: stop }, stopBtn) : /* @__PURE__ */ React.createElement("span", { role: "button", onClick: pause }, pauseBtn), !useStopOverPause && stopBtn && /* @__PURE__ */ React.createElement("span", { role: "button", onClick: stop }, stopBtn), /* @__PURE__ */ React.createElement(Text, { ref: sourceRef, style: { display: "none" } })); } //#endregion export { HighlightedText, Speech as default, useQueue, useSpeak, useSpeech, useVoices };