Implement complete modularization of reader code by separating format-specific functionality into dedicated modules. This replaces the monolithic structure with a clean, maintainable architecture that separates concerns by format type. ## New Architecture ### Format-Specific Modules - **formats/reflowable/**: EPUB, FB2, TXT, HTML (page-based pagination) - types.ts: Shared type definitions for reflowable formats - page-calculator.ts: Word-count based pagination with HTML slicing - navigation.ts: Page-based navigation logic - progress-tracker.ts: CFI-based progress tracking - content-renderer.ts: DOM rendering for page content - parser.ts: Unified parser interface for all reflowable formats - ebook/**: Migrated ebook-specific features - **formats/pdf/**: PDF format support - Core PDF functionality (navigation, text selection, annotations) - Advanced features (bookmarks, search, outlines, dual-page) - Page cache and rendering optimizations - **formats/comic/**: Comic format support - Background color, chapter markers, page caching - Page ordering, gap adjustments - **formats/manga/**: Manga format support - RTL navigation, vertical scrolling, reading direction ## Key Improvements 1. **Separation of Concerns**: Each format has its own dedicated module 2. **No Circular Dependencies**: Clean import structure 3. **Type Safety**: Comprehensive TypeScript types throughout 4. **Functional Programming**: Pure functions, no OOP complexity 5. **Scalability**: Easy to add new formats without touching core code ## Migration Path - Old format-specific code in reader/, ebook/, pdf/, comic/, manga/ - New code in formats/[format]/ structure - Maintains backward compatibility during transition - Core reader logic remains format-agnostic This change enables the implementation of page-based pagination for reflowable formats while keeping PDF, comic, and manga functionality unchanged.
137 lines
3.0 KiB
TypeScript
137 lines
3.0 KiB
TypeScript
// Full-text search within PDF documents
|
|
|
|
import { PDFDocumentProxy } from "pdfjs-dist";
|
|
|
|
interface SearchResult {
|
|
pageNumber: number;
|
|
text: string;
|
|
index: number;
|
|
context: string;
|
|
}
|
|
|
|
// Full-text search within PDF documents
|
|
// Procedural implementation (no OOP)
|
|
|
|
interface SearchResult {
|
|
pageNumber: number;
|
|
text: string;
|
|
index: number;
|
|
context: string;
|
|
}
|
|
|
|
interface PDFSearchState {
|
|
doc: PDFDocumentProxy | null;
|
|
searchResults: SearchResult[];
|
|
currentResultIndex: number;
|
|
}
|
|
|
|
async function initializePDFSearch(
|
|
doc: PDFDocumentProxy,
|
|
): Promise<PDFSearchState> {
|
|
return {
|
|
doc,
|
|
searchResults: [],
|
|
currentResultIndex: 0,
|
|
};
|
|
}
|
|
|
|
async function searchPDF(
|
|
state: PDFSearchState,
|
|
query: string,
|
|
): Promise<PDFSearchState> {
|
|
if (!state.doc) return state;
|
|
|
|
const searchResults: SearchResult[] = [];
|
|
const lowerQuery = query.toLowerCase();
|
|
|
|
for (let pageNum = 1; pageNum <= state.doc.numPages; pageNum++) {
|
|
const page = await state.doc.getPage(pageNum);
|
|
const textContent = await page.getTextContent();
|
|
|
|
let fullText = "";
|
|
const textItems = textContent.items.map((item) => {
|
|
if (typeof item === "string") return "";
|
|
fullText += item.str;
|
|
return item.str;
|
|
});
|
|
|
|
const pageText = textItems.join(" ");
|
|
const matches = findSearchMatches(pageText, lowerQuery, pageNum);
|
|
|
|
searchResults.push(...matches);
|
|
}
|
|
|
|
return { ...state, searchResults };
|
|
}
|
|
|
|
function findSearchMatches(
|
|
text: string,
|
|
query: string,
|
|
pageNumber: number,
|
|
): SearchResult[] {
|
|
const matches: SearchResult[] = [];
|
|
const lowerText = text.toLowerCase();
|
|
let index = 0;
|
|
|
|
while ((index = lowerText.indexOf(query, index)) !== -1) {
|
|
const start = Math.max(0, index - 50);
|
|
const end = Math.min(text.length, index + query.length + 50);
|
|
const context = text.slice(start, end);
|
|
|
|
matches.push({
|
|
pageNumber,
|
|
text: text.slice(index, index + query.length),
|
|
index,
|
|
context,
|
|
});
|
|
|
|
index += query.length;
|
|
}
|
|
|
|
return matches;
|
|
}
|
|
|
|
function goToNextSearchResult(
|
|
state: PDFSearchState,
|
|
): PDFSearchState & { result: SearchResult | null } {
|
|
if (state.searchResults.length === 0) {
|
|
return { ...state, result: null };
|
|
}
|
|
|
|
const newIndex = (state.currentResultIndex + 1) % state.searchResults.length;
|
|
return {
|
|
...state,
|
|
currentResultIndex: newIndex,
|
|
result: state.searchResults[newIndex],
|
|
};
|
|
}
|
|
|
|
function goToPreviousSearchResult(
|
|
state: PDFSearchState,
|
|
): PDFSearchState & { result: SearchResult | null } {
|
|
if (state.searchResults.length === 0) {
|
|
return { ...state, result: null };
|
|
}
|
|
|
|
const newIndex =
|
|
(state.currentResultIndex - 1 + state.searchResults.length) %
|
|
state.searchResults.length;
|
|
return {
|
|
...state,
|
|
currentResultIndex: newIndex,
|
|
result: state.searchResults[newIndex],
|
|
};
|
|
}
|
|
|
|
function getSearchResultCount(state: PDFSearchState): number {
|
|
return state.searchResults.length;
|
|
}
|
|
|
|
function clearSearchResults(state: PDFSearchState): PDFSearchState {
|
|
return {
|
|
...state,
|
|
searchResults: [],
|
|
currentResultIndex: 0,
|
|
};
|
|
}
|