fix(reader): optimize rendering performance, prevent browser freeze on large technical books

- extract_outline_entries: filter PDF bookmarks by depth and major structural headings, avoiding 550 micro-snippets
- renderContinuousBook: load current chapter and seamlessly append next chapters on scroll (virtual continuous) instead of dumping all chapters into DOM synchronously
- renderInteractiveSpans: use native HTML template fragment instead of new DOMParser per paragraph
- event delegation: handle word clicks on readerContentBody instead of adding hundreds of thousands of individual DOM event listeners
- consolidate existing CCNA book in DB into 68 clean chapters
This commit is contained in:
2026-10-06 11:23:21 +03:00
parent 46d6973365
commit c4a6edee37
3 changed files with 70 additions and 38 deletions
+41 -8
View File
@@ -513,15 +513,17 @@ CLI_PROMPT_RE = re.compile(
def extract_outline_entries(outline, reader) -> List[Tuple[str, int]]:
"""
Recursively extracts bookmarks / outline entries from PDF.
Returns a list of (title, 0-indexed page_number).
Recursively extracts bookmarks / outline entries from PDF respecting hierarchy depth.
For books with deep trees (Parts -> Chapters -> Sections -> Subsections),
prioritizes major structural headings and prevents fragmenting chapters into
hundreds of tiny 1-page snippets.
"""
entries: List[Tuple[str, int]] = []
raw_entries: List[Tuple[str, int, int]] = []
def _traverse(items):
def _traverse(items, depth=0):
for item in items:
if isinstance(item, list):
_traverse(item)
_traverse(item, depth + 1)
else:
try:
title = getattr(item, "title", None) or (item.get("/Title") if hasattr(item, "get") else None)
@@ -529,13 +531,44 @@ def extract_outline_entries(outline, reader) -> List[Tuple[str, int]]:
if title and page_num is not None:
clean_title = str(title).strip()
if clean_title:
entries.append((clean_title, int(page_num)))
raw_entries.append((clean_title, int(page_num), depth))
except Exception:
continue
if outline:
_traverse(outline)
return entries
_traverse(outline, 0)
if not raw_entries:
return []
# If total entries is reasonable (<= 50), return all
if len(raw_entries) <= 50:
return [(t, p) for t, p, _ in raw_entries]
# Check depth distribution
d0_entries = [e for e in raw_entries if e[2] == 0]
d1_entries = [e for e in raw_entries if e[2] <= 1]
# If top-level (depth 0) already has >= 10 chapters, use depth 0
if len(d0_entries) >= 10:
return [(t, p) for t, p, _ in d0_entries]
# Otherwise, if depth <= 1 gives a good number of chapters (10 to 80), use depth <= 1
if len(d1_entries) <= 80:
return [(t, p) for t, p, _ in d1_entries]
# If still too many entries (> 80), filter by major chapter keywords
major_re = re.compile(
r"^(cover|title\s+page|copyright|contents|introduction|your\s+study\s+plan|"
r"part\s+[0-9ivxlcdm]+|chapter\s+\d+|appendix\s+[a-z]|glossary|index)",
re.IGNORECASE
)
major_entries = [e for e in d1_entries if major_re.match(e[0])]
if len(major_entries) >= 10:
return [(t, p) for t, p, _ in major_entries]
return [(t, p) for t, p, _ in d1_entries[:80]]
def clean_page_lines(raw_pages_text: List[str]) -> List[str]: