diff --git a/src/lib/actions/bionicReading.js b/src/lib/actions/bionicReading.js new file mode 100644 index 0000000..152a28c --- /dev/null +++ b/src/lib/actions/bionicReading.js @@ -0,0 +1,70 @@ +// src/lib/actions/bionicReading.js + +function applyBionic(node) { + const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT); + const textNodes = []; + let current; + while ((current = walker.nextNode())) textNodes.push(current); + + for (const textNode of textNodes) { + 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) { + 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, enabled) { + let observer = 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() { + 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) { + enabled = newEnabled; + refresh(); + }, + destroy() { + if (observer) observer.disconnect(); + stripBionic(node); + } + }; +}