'use strict'; // --------------------------------------------------------------------------- // Reader Search // --------------------------------------------------------------------------- let _readerSearchDebounce = null; function toggleReaderSearch() { const overlay = $('reader-overlay'); const contentEl = $('reader-content'); if (!overlay || !contentEl) return; const existing = document.getElementById('reader-search-bar'); if (existing) { existing.remove(); readerSearchOpen = false; clearReaderSearch(); return; } readerSearchOpen = true; const bar = document.createElement('div'); bar.id = 'reader-search-bar'; bar.className = 'reader-search-bar'; bar.innerHTML = ` `; overlay.insertBefore(bar, $('reader-body-row')); const input = bar.querySelector('#reader-search-input'); input.focus(); input.addEventListener('input', () => { clearTimeout(_readerSearchDebounce); _readerSearchDebounce = setTimeout(() => doReaderSearch(input.value.trim()), 300); }); input.addEventListener('keydown', e => { if (e.key === 'Enter') { e.shiftKey ? readerSearchPrev() : readerSearchNext(); } if (e.key === 'Escape') { toggleReaderSearch(); } }); bar.querySelector('#rs-search-prev').addEventListener('click', readerSearchPrev); bar.querySelector('#rs-search-next').addEventListener('click', readerSearchNext); bar.querySelector('#rs-search-clear').addEventListener('click', toggleReaderSearch); } async function doReaderSearch(query) { const contentEl = $('reader-content'); if (!contentEl) return; const countEl = document.getElementById('rs-search-count'); clearReaderSearchHighlights(); searchMatches = []; searchMatchIndex = -1; if (!query) { if (countEl) countEl.textContent = ''; return; } if (!currentPdfDoc) { // EPUB: snapshot original content if (!searchOriginalContent) { searchOriginalContent = contentEl.innerHTML; } else { contentEl.innerHTML = searchOriginalContent; applyHighlightsToContent(); } const walker = document.createTreeWalker(contentEl, NodeFilter.SHOW_TEXT); const lq = query.toLowerCase(); const ranges = []; let node; while ((node = walker.nextNode())) { const text = node.textContent; const lt = text.toLowerCase(); let idx = 0; while ((idx = lt.indexOf(lq, idx)) !== -1) { const range = document.createRange(); range.setStart(node, idx); range.setEnd(node, idx + query.length); ranges.push(range); idx += query.length; } } // Insert marks in reverse to preserve range validity for (let i = ranges.length - 1; i >= 0; i--) { try { const mark = document.createElement('mark'); mark.className = 'reader-search-match'; ranges[i].surroundContents(mark); searchMatches.unshift(mark); } catch (e) {} } } else { // PDF: the whole document, page by page (not only the pages drawn now). _pdfSearchAll(query); return; } if (countEl) countEl.textContent = searchMatches.length ? `1 / ${searchMatches.length}` : '0'; if (searchMatches.length) { searchMatchIndex = 0; scrollToSearchMatch(0); } } function clearReaderSearchHighlights() { if (!currentPdfDoc) { // EPUB: restore from snapshot if (searchOriginalContent !== null) { const contentEl = $('reader-content'); if (contentEl) { contentEl.innerHTML = searchOriginalContent; applyHighlightsToContent(); } searchOriginalContent = null; } else { // Just remove marks without full restore document.querySelectorAll('mark.reader-search-match').forEach(m => { const parent = m.parentNode; while (m.firstChild) parent.insertBefore(m.firstChild, m); parent.removeChild(m); }); } } else { _pdfSearchStop(); } searchMatches = []; searchMatchIndex = -1; } function clearReaderSearch() { clearTimeout(_readerSearchDebounce); clearReaderSearchHighlights(); readerSearchOpen = false; const countEl = document.getElementById('rs-search-count'); if (countEl) countEl.textContent = ''; } function scrollToSearchMatch(idx) { if (!searchMatches.length) return; if (currentPdfDoc) { _pdfShowMatch(idx); return; } _suppressScrollJumpDetect(); searchMatches.forEach((m, i) => m.classList.toggle('active', i === idx)); searchMatches[idx].scrollIntoView({behavior: 'smooth', block: 'center'}); const countEl = document.getElementById('rs-search-count'); if (countEl) countEl.textContent = `${idx + 1} / ${searchMatches.length}`; } // --------------------------------------------------------------------------- // PDF search: through the text of every page (58-pdf.js keeps it), in order; the matches // marked in the text layer of the pages drawn, and of those drawn later. // --------------------------------------------------------------------------- let _pdfSearchState = null; // {query, matches: [{page, start, end}], done} let _pdfSearchGen = 0; function _pdfSearchCount() { const countEl = document.getElementById('rs-search-count'); const st = _pdfSearchState; if (!countEl || !st) return; const more = st.done ? '' : ' …'; countEl.textContent = st.matches.length ? `${searchMatchIndex >= 0 ? searchMatchIndex + 1 : '–'} / ${st.matches.length}${more}` : (st.done ? '0' : '…'); } async function _pdfSearchAll(query) { const pdf = currentPdfDoc; const gen = ++_pdfSearchGen; const lq = query.toLowerCase(); const st = {query, matches: [], done: false}; _pdfSearchState = st; searchMatches = st.matches; searchMatchIndex = -1; _pdfSearchCount(); // The first match from the page being read on is shown as soon as it's found. const from = pdfCurrentPage || 1; for (let n = 1; n <= pdf.numPages; n++) { const tc = await _pdfPageText(pdf, n); if (gen !== _pdfSearchGen || pdf !== currentPdfDoc) return; const {str} = _pdfPageString(tc); const before = st.matches.length; let i = 0; while (lq && (i = str.indexOf(lq, i)) !== -1) { st.matches.push({page: n, start: i, end: i + lq.length}); i += lq.length; } if (st.matches.length > before) { const e = _pdfView && _pdfView.entries[n - 1]; if (e) _pdfSearchMarkPage(e); if (searchMatchIndex < 0 && n >= from) { searchMatchIndex = before; _pdfShowMatch(before); } } if (n % 20 === 0) _pdfSearchCount(); } st.done = true; if (searchMatchIndex < 0 && st.matches.length) { searchMatchIndex = 0; _pdfShowMatch(0); } _pdfSearchCount(); } /** Marks of search [st] in page [e]'s text layer (none when no search is on). */ function _pdfSearchMarkPage(e) { if (!e.textDivs || !e.textInfo) return; if (e.marked) { for (const i of e.marked) if (e.textDivs[i]) e.textDivs[i].textContent = e.textInfo.items[i].str; e.marked = null; } const st = _pdfSearchState; if (!st) return; const {starts} = e.textInfo.string; const items = e.textInfo.items; // Per span, the pieces of it that match (one match can run over several spans). const pieces = new Map(); st.matches.forEach((m, idx) => { if (m.page !== e.num) return; for (let i = 0; i < items.length; i++) { const s = starts[i], len = (items[i].str || '').length; if (s >= m.end) break; if (s + len <= m.start || !len) continue; if (!pieces.has(i)) pieces.set(i, []); pieces.get(i).push([Math.max(m.start, s) - s, Math.min(m.end, s + len) - s, idx]); } }); e.marked = []; pieces.forEach((list, i) => { const div = e.textDivs[i]; if (!div) return; const str = items[i].str; div.textContent = ''; let at = 0; for (const [a, b, idx] of list) { if (a > at) div.appendChild(document.createTextNode(str.slice(at, a))); const mark = document.createElement('mark'); mark.className = 'reader-search-match' + (idx === searchMatchIndex ? ' active' : ''); mark.dataset.m = idx; mark.textContent = str.slice(a, b); div.appendChild(mark); at = b; } if (at < str.length) div.appendChild(document.createTextNode(str.slice(at))); e.marked.push(i); }); } /** Go to match [idx]: its page, and (scrolling) the match itself into view. */ async function _pdfShowMatch(idx) { const st = _pdfSearchState; const m = st && st.matches[idx]; if (!m) return; const gen = _pdfSearchGen; _pdfSearchCount(); document.querySelectorAll('.pdf-text-layer mark.reader-search-match').forEach(mk => { mk.classList.toggle('active', +mk.dataset.m === idx); }); const contentEl = $('reader-content'); const shown = readerSettings.pdfPaginated ? pdfCurrentPage : (contentEl ? _pdfTopPage(contentEl) : 0); if (m.page !== shown || readerSettings.pdfPaginated) pdfGoToPage(m.page); if (readerSettings.pdfPaginated || !contentEl) return; // Scrolling: the page is drawn (and its text laid over it) once it's near the view. for (let t = 0; t < 50; t++) { if (gen !== _pdfSearchGen || searchMatchIndex !== idx) return; const mk = contentEl.querySelector(`#pdf-page-${m.page} mark[data-m="${idx}"]`); if (mk) { _suppressScrollJumpDetect(); mk.scrollIntoView({block: 'center'}); return; } await new Promise(r => setTimeout(r, 100)); } } function _pdfSearchStop() { _pdfSearchGen++; _pdfSearchState = null; if (_pdfView) _pdfView.entries.forEach(e => { if (e.marked) _pdfSearchMarkPage(e); }); } function readerSearchNext() { if (!searchMatches.length) return; searchMatchIndex = (searchMatchIndex + 1) % searchMatches.length; scrollToSearchMatch(searchMatchIndex); } function readerSearchPrev() { if (!searchMatches.length) return; searchMatchIndex = (searchMatchIndex - 1 + searchMatches.length) % searchMatches.length; scrollToSearchMatch(searchMatchIndex); }