Dynamic pagination (DOM-measured, reflows on layout)
Parent: key-designs
It is DOM-measurement-based, not character-budget. The reader renders each candidate page into an offscreen node and reads real scrollHeight to decide overflow — so page boundaries are a function of actually rendered text under the current font + viewport, and shift whenever those change. That's what makes it dynamic.
The probe (application-services/streamingProbe/). A StreamingProbe interface: appendAndCheck(sentence) pushes onto a candidate page, renders, returns true on overflow; reset(seed)/clear(). Two impls behind it (a facade routes by book size / device):
domStreamingProbe(web, main-thread):flushSynces classified paragraphs into a.reader-probecontainer; the fit oracle isscrollHeight <= clientHeight - PROBE_PADDING_PX(64). Deliberately O(N) measures per page (replacing an older binary search), with a 50 ms time-slicedmacrotaskYieldso whole-book pagination doesn't freeze the reader.pretextStreamingProbe(Web Worker, off-main-thread): measures via@chenglou/pretextfor mobile / large books where per-sentenceflushSyncwould jank.
Assigning pageNumber. usecases/paginate.ts is an async generator driving the probe one sentence at a time; on overflow it commits the fitting range and re-seeds with the overflowing sentence, stamping each CachedSentence with sentenceIdx + pageNumber. Three transitions: chapter-heading hard break, normal overflow, and a "single sentence taller than a page" progress guarantee.
Chapter breaks force boundaries without measuring: paginationChapterBreaks.collectChapterBreaks merges TOC + NCX/PDF-outline offsets (collapsing clusters closer than 200 chars / 5 sentences); a chapter-heading sentence mid-page ends the page before it.
The dynamic loop (the reflow). usePaginationCache watches {fontSize, fontFamily, pageWidth, pageHeight, contentHash} (viewport from a ResizeObserver), debounces (50 ms first / 300 ms after), and pickComputeFn dispatches:
- cache ready +
LayoutKeymatches → noop; - cache ready + layout differs →
repaginate— reuse sentence text/sanitize/LCS, re-run only the probe to reassignpageNumber(the font/resize reflow path); - no cache / version-invalidated → full compute (sanitize + raw↔display LCS + probe).
The persisted
LayoutKey {fontSize, pageWidth, pageHeight, fontFamily}is compared on every open.
Versioning. PAGINATION_VERSION (currently 7, with a bump history) is deliberately separate from SANITIZER_VERSION, so a layout-logic change forces re-pagination without rebuilding the expensive content cache.
Page is transient (entities/Page.ts): built on demand from a contiguous same-pageNumber slice; only the current page + a ±10-page window lives in RAM; offset translation rides each sentence's displayToRaw (raw-display-cursor).