'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);
}