FEATURE Optimizing article reader behaviour

This commit is contained in:
Nonimart 2025-09-30 11:52:26 +02:00
parent 52db4fc6fa
commit 2b903d7a1c

View File

@ -18,8 +18,7 @@ const DEFAULT_CONFIG: ReaderConfig = {
pitch: 1.1, pitch: 1.1,
volume: 1, volume: 1,
highlightColor: '#f1fcf9', highlightColor: '#f1fcf9',
highlightStyle: highlightStyle: 'background-color: #136F63; padding: 2px 4px; border-radius: 3px; color:white;',
'background-color: #f1fcf9; padding: 2px 4px; border-radius: 3px; color: #136F63;',
}; };
export default function handleArticleReader() { export default function handleArticleReader() {
@ -31,8 +30,6 @@ export default function handleArticleReader() {
let utterance: SpeechSynthesisUtterance; let utterance: SpeechSynthesisUtterance;
let voices: SpeechSynthesisVoice[] = []; let voices: SpeechSynthesisVoice[] = [];
let thomasVoice: SpeechSynthesisVoice | null = null; let thomasVoice: SpeechSynthesisVoice | null = null;
let originalContent = '';
let words: string[] = [];
let currentWordIndex = 0; let currentWordIndex = 0;
/* ******************************************************** /* ********************************************************
@ -77,37 +74,132 @@ export default function handleArticleReader() {
} }
} }
// Fonctions de highlighting // ✅ HIGHLIGHTING AVANCÉ - Préserve la structure HTML
function highlightWord(index: number): void { let textNodes: Text[] = [];
if (!article || index >= words.length) return; let wordBoundaries: { node: Text; startOffset: number; endOffset: number; word: string }[] = [];
// Restaurer le contenu original si c'est le premier mot function collectTextNodes(element: Element): Text[] {
if (index === 0) { const nodes: Text[] = [];
article.innerHTML = originalContent; const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, {
} acceptNode: (node) => {
// Ignorer les nœuds texte vides ou uniquement des espaces
// Créer le nouveau HTML avec le mot highlighted return node.textContent?.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
const highlightedWords = words.map((word, i) => { },
if (i === index) {
return `<span class="speech-highlight" style="${DEFAULT_CONFIG.highlightStyle}">${word}</span>`;
}
return word;
}); });
article.innerHTML = highlightedWords.join(' '); let node;
while ((node = walker.nextNode())) {
nodes.push(node as Text);
}
return nodes;
}
function buildWordBoundaries(): void {
wordBoundaries = [];
textNodes = collectTextNodes(article as Element);
textNodes.forEach((textNode) => {
const text = textNode.textContent || '';
const wordRegex = /\S+/g; // Mots (non-espaces)
let match;
while ((match = wordRegex.exec(text)) !== null) {
wordBoundaries.push({
node: textNode,
startOffset: match.index,
endOffset: match.index + match[0].length,
word: match[0],
});
}
});
// 🐛 DEBUG: Afficher les mots détectés
console.log(
'Mots détectés dans le DOM:',
wordBoundaries.map((b) => b.word)
);
}
function highlightWord(index: number): void {
if (!article || index >= wordBoundaries.length) {
console.log('❌ Highlight impossible:', { index, totalWords: wordBoundaries.length });
return;
}
// Supprimer le highlight précédent et reconstruire les boundaries
removeHighlight();
buildWordBoundaries(); // ✅ Recalculer après cleanup
// Vérifier à nouveau après reconstruction
if (index >= wordBoundaries.length) {
console.warn('❌ Index invalide après reconstruction:', {
index,
totalWords: wordBoundaries.length,
});
return;
}
const boundary = wordBoundaries[index];
console.log('🎯 Highlighting mot #' + index + ':', boundary.word);
// Vérifier que le nœud et les offsets sont encore valides
if (!boundary.node || !boundary.node.textContent) {
console.warn('❌ Nœud texte invalide');
return;
}
if (
boundary.startOffset > boundary.node.textContent.length ||
boundary.endOffset > boundary.node.textContent.length
) {
console.warn('❌ Offsets invalides:', {
startOffset: boundary.startOffset,
endOffset: boundary.endOffset,
nodeLength: boundary.node.textContent.length,
});
return;
}
try {
const range = document.createRange();
range.setStart(boundary.node, boundary.startOffset);
range.setEnd(boundary.node, boundary.endOffset);
// Créer le span de highlight
const highlightSpan = document.createElement('span');
highlightSpan.className = 'speech-highlight';
highlightSpan.style.cssText = DEFAULT_CONFIG.highlightStyle;
range.surroundContents(highlightSpan);
console.log('✅ Highlight réussi pour:', boundary.word);
} catch (error) {
console.warn('⚠️ Erreur lors du highlight:', error, boundary);
}
} }
function removeHighlight(): void { function removeHighlight(): void {
if (article && originalContent) { if (!article) return;
article.innerHTML = originalContent;
const highlights = article.querySelectorAll('.speech-highlight');
highlights.forEach((highlight) => {
const parent = highlight.parentNode;
if (parent) {
// Remplacer le span par son contenu
while (highlight.firstChild) {
parent.insertBefore(highlight.firstChild, highlight);
} }
parent.removeChild(highlight);
// Normaliser pour fusionner les nœuds texte adjacents
parent.normalize();
}
});
} }
// Création de l'utterance // Création de l'utterance
function createUtterance(text: string): SpeechSynthesisUtterance { function createUtterance(text: string): SpeechSynthesisUtterance {
// Sauvegarder le contenu original et préparer les mots // ✅ Construire la carte des mots avec leur position dans le DOM
originalContent = article ? article.innerHTML : ''; buildWordBoundaries();
words = text.split(/\s+/).filter((word) => word.trim().length > 0);
currentWordIndex = 0; currentWordIndex = 0;
utterance = new SpeechSynthesisUtterance(text); utterance = new SpeechSynthesisUtterance(text);
@ -123,9 +215,9 @@ export default function handleArticleReader() {
utterance.voice = thomasVoice; utterance.voice = thomasVoice;
} }
// Event pour highlighter les mots pendant la lecture // Event pour highlighter les mots pendant la lecture (basé sur les boundaries DOM)
utterance.onboundary = (event) => { utterance.onboundary = (event) => {
if (event.name === 'word') { if (event.name === 'word' && currentWordIndex < wordBoundaries.length) {
highlightWord(currentWordIndex); highlightWord(currentWordIndex);
currentWordIndex++; currentWordIndex++;
} }