// BB4S Search Highlighter - Fixed for quoted phrases and line breaks (function() { 'use strict'; // Get highlight query from URL function getHighlightQuery() { const params = new URLSearchParams(window.location.search); return params.get('highlight'); } // Escape special regex characters function escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function highlightProximity(phrase, distance) { const words = phrase.toLowerCase().split(/\s+/); if (words.length === 0) return; // Get all text nodes const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { if (node.parentElement.tagName === 'SCRIPT' || node.parentElement.tagName === 'STYLE' || node.parentElement.tagName === 'MARK') { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; } } ); // Collect all text and track nodes let fullText = ''; const nodeMap = []; let node; while (node = walker.nextNode()) { const startPos = fullText.length; fullText += node.nodeValue; nodeMap.push({ node, start: startPos, end: fullText.length }); } const textWords = fullText.toLowerCase().split(/\s+/); const matches = []; // Find proximity matches for (let i = 0; i < textWords.length; i++) { if (textWords[i].includes(words[0])) { let allFound = true; let maxIdx = i; for (let j = 1; j < words.length; j++) { let found = false; for (let k = i + 1; k < Math.min(i + distance + 1, textWords.length); k++) { if (textWords[k].includes(words[j])) { found = true; maxIdx = Math.max(maxIdx, k); break; } } if (!found) { allFound = false; break; } } if (allFound) { // Mark range from word i to maxIdx matches.push({ start: i, end: maxIdx }); } } } console.log('Proximity matches found:', matches.length); // Highlight each word in matches matches.forEach(match => { for (let i = match.start; i <= match.end; i++) { const word = words[i % words.length] || textWords[i]; highlightWord(word); } }); // Fallback: just highlight individual words words.forEach(word => highlightWord(word)); addHighlightStyles(); const allMatches = document.querySelectorAll('mark.search-highlight'); if (allMatches.length > 0) { highlightCurrentMatch(); setupNavigation(); } } function highlightWord(word) { const regex = new RegExp('\\b' + escapeRegex(word) + '\\b', 'gi'); const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { if (node.parentElement.tagName === 'SCRIPT' || node.parentElement.tagName === 'STYLE' || node.parentElement.tagName === 'MARK') { return NodeFilter.FILTER_REJECT; } return regex.test(node.nodeValue) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; } } ); const nodesToReplace = []; let node; while (node = walker.nextNode()) { nodesToReplace.push(node); } nodesToReplace.forEach(function(textNode) { const parent = textNode.parentNode; const text = textNode.nodeValue; const fragment = document.createDocumentFragment(); let lastIndex = 0; let match; regex.lastIndex = 0; while ((match = regex.exec(text)) !== null) { if (match.index > lastIndex) { fragment.appendChild(document.createTextNode(text.slice(lastIndex, match.index))); } const mark = document.createElement('mark'); mark.className = 'search-highlight'; mark.textContent = match[0]; fragment.appendChild(mark); lastIndex = match.index + match[0].length; } if (lastIndex < text.length) { fragment.appendChild(document.createTextNode(text.slice(lastIndex))); } parent.replaceChild(fragment, textNode); }); } function highlightMatches(query) { if (!query) return; // Check for proximity search: "term1 term2"~5 const proximityMatch = query.match(/"([^"]+)"~(\d+)/); if (proximityMatch) { const phrase = proximityMatch[1]; const distance = parseInt(proximityMatch[2]); console.log('Proximity search:', phrase, 'within', distance, 'words'); highlightProximity(phrase, distance); return; } // Extract quoted phrases first const phraseMatches = query.match(/"([^"]+)"/g) || []; const phrases = phraseMatches.map(p => p.replace(/"/g, '').trim()); // Remove quoted phrases from query and get remaining terms let remainingQuery = query; phraseMatches.forEach(p => { remainingQuery = remainingQuery.replace(p, ''); }); // Get individual search terms (not in quotes) const terms = remainingQuery .trim() .split(/\s+/) .filter(t => t && t.length > 0 && !['AND', 'NOT', 'OR'].includes(t.toUpperCase())); // Combine phrases and terms const allSearchTerms = [...phrases, ...terms]; console.log('Search terms:', allSearchTerms); if (allSearchTerms.length === 0) return; // Create regex pattern - allow any whitespace between words const pattern = allSearchTerms.map(term => { const escaped = escapeRegex(term); // Replace spaces with \s+ to match any whitespace including line breaks return escaped.replace(/\s+/g, '\\s+'); }).join('|'); const regex = new RegExp('(' + pattern + ')', 'gi'); console.log('Regex pattern:', regex); // Walk through all text nodes in body const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { if (node.parentElement.tagName === 'SCRIPT' || node.parentElement.tagName === 'STYLE' || node.parentElement.tagName === 'MARK') { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; } } ); const nodesToReplace = []; let node; while (node = walker.nextNode()) { if (regex.test(node.nodeValue)) { nodesToReplace.push(node); } } console.log('Nodes to highlight:', nodesToReplace.length); // Replace text nodes with highlighted spans nodesToReplace.forEach(function(textNode) { const parent = textNode.parentNode; const text = textNode.nodeValue; // Split by matches and create new nodes const fragment = document.createDocumentFragment(); let lastIndex = 0; let match; regex.lastIndex = 0; while ((match = regex.exec(text)) !== null) { // Add text before match if (match.index > lastIndex) { fragment.appendChild(document.createTextNode(text.slice(lastIndex, match.index))); } // Add highlighted match const mark = document.createElement('mark'); mark.className = 'search-highlight'; mark.textContent = match[0]; fragment.appendChild(mark); lastIndex = match.index + match[0].length; } // Add remaining text if (lastIndex < text.length) { fragment.appendChild(document.createTextNode(text.slice(lastIndex))); } parent.replaceChild(fragment, textNode); }); // Add CSS for highlights addHighlightStyles(); // Set up navigation const matches = document.querySelectorAll('mark.search-highlight'); if (matches.length > 0) { highlightCurrentMatch(); setupNavigation(); } } function addHighlightStyles() { if (document.getElementById('search-highlight-styles')) return; const style = document.createElement('style'); style.id = 'search-highlight-styles'; style.textContent = ` mark.search-highlight { background-color: yellow; color: black; padding: 2px 0; } mark.search-highlight.current { background-color: orange; font-weight: bold; } `; document.head.appendChild(style); } function highlightCurrentMatch() { const matches = document.querySelectorAll('mark.search-highlight'); matches.forEach(m => m.classList.remove('current')); if (matches.length > 0) { matches[0].classList.add('current'); matches[0].scrollIntoView({ behavior: 'smooth', block: 'center' }); } } function setupNavigation() { // Arrow key navigation document.addEventListener('keydown', function(e) { if (e.key === 'ArrowDown' || e.key === 'ArrowRight') { nextMatch(); e.preventDefault(); } else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') { previousMatch(); e.preventDefault(); } }); } function nextMatch() { const matches = Array.from(document.querySelectorAll('mark.search-highlight')); const current = document.querySelector('mark.search-highlight.current'); if (!current || matches.length === 0) return; const currentIndex = matches.indexOf(current); const nextIndex = (currentIndex + 1) % matches.length; current.classList.remove('current'); matches[nextIndex].classList.add('current'); matches[nextIndex].scrollIntoView({ behavior: 'smooth', block: 'center' }); } function previousMatch() { const matches = Array.from(document.querySelectorAll('mark.search-highlight')); const current = document.querySelector('mark.search-highlight.current'); if (!current || matches.length === 0) return; const currentIndex = matches.indexOf(current); const prevIndex = currentIndex > 0 ? currentIndex - 1 : matches.length - 1; current.classList.remove('current'); matches[prevIndex].classList.add('current'); matches[prevIndex].scrollIntoView({ behavior: 'smooth', block: 'center' }); } // Auto-run on page load window.addEventListener('load', function() { const query = getHighlightQuery(); if (query) { console.log('Highlighting query:', query); setTimeout(() => highlightMatches(query), 100); } }); })();