// src/lib/actions/bionicReading.js function applyBionic(node: HTMLElement) { const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT); const textNodes: Text[] = []; let current: Node | null; while ((current = walker.nextNode())) { if (current.nodeType === Node.TEXT_NODE) textNodes.push(current as Text); } for (const textNode of textNodes) { if (textNode.textContent == null) continue; const words = textNode.textContent.split(/(\s+)/); if (words.length <= 1 && !words[0].trim()) continue; const fragment = document.createDocumentFragment(); for (const word of words) { if (!word.trim() || word.length < 2) { fragment.appendChild(document.createTextNode(word)); continue; } const boldLen = Math.min(3, Math.ceil(word.length / 2)); const b = document.createElement('b'); b.textContent = word.slice(0, boldLen); fragment.appendChild(b); fragment.appendChild(document.createTextNode(word.slice(boldLen))); } textNode.parentNode?.replaceChild(fragment, textNode); } } function stripBionic(node: HTMLElement) { const bolds = node.querySelectorAll("b"); for (const b of bolds) { const text = document.createTextNode(b.textContent ?? ""); b.parentNode?.replaceChild(text, b); } node.normalize(); // merge adjacent text nodes } export function bionicReading(node: HTMLElement, enabled: boolean) { let observer: MutationObserver | null = null; function refresh() { // Disconnect observer while we mutate, to avoid infinite loop if (observer) observer.disconnect(); stripBionic(node); if (enabled) applyBionic(node); if (observer) observe(); } function observe() { if (!observer) return; observer.observe(node, { childList: true, characterData: true, subtree: true }); } // MutationObserver catches Svelte re-rendering DOM content underneath us // (new transcript segments, task name edits, etc.) observer = new MutationObserver(refresh); if (enabled) applyBionic(node); observe(); return { update(newEnabled: boolean) { enabled = newEnabled; refresh(); }, destroy() { if (observer) observer.disconnect(); stripBionic(node); } }; }