refactor: streaming loader, hierarchical TOC, parser robustness

Rework the reader around a clean BookInfo interface and lazy chapter loading.

Types/parser:
- BookInfo now splits into loadStructure (cheap: spine refs + TOC tree) and renderChapter (one chapter on demand). EpubEnv carries the parsed archive.
- resolveZipPath collapses ./.. and strips #fragments (normalise does not), so relative hrefs like ../Images/x.jpg resolve; used for chapters and images.
- decodeEntry tolerates encodings: UTF-8/UTF-16 BOMs, Latin-1 fallback instead of throwing on invalid bytes.
- Skip spine items with no manifest entry instead of throwing an error.
- Scan the zip for a .opf if container.xml is missing.
- Image inlining rewrites the src URL string, not the whole <img> tag (scalpel
  re-serialises tags, so a whole-tag replace silently missed self-closing imgs).

Navigation:
- Hierarchical TOC: ncxToc (EPUB 2), navToc (EPUB 3), guideToc (<guide> fallback). Nested scrapers use atDepth 1 to take only direct children.

UI:
- Streaming: render the opened chapter on demand, cache it, preload neighbours so arrow paging is instant with a placeholder that covers uncached jumps.
- Sidebar shows the TOC tree with CSS styling. Clicks scroll to #fragments.
- wrapHtml wraps each body in a full HTML document for reliable WebKit render.

Test: 'svitak-test <book> <idx>' dumps one rendered chapter. cabal test stanza fixed to list all needed modules/deps.
This commit is contained in:
Marko Andjelic 2026-06-26 03:41:20 +01:00
commit 2011d638b0
Signed by: marko
GPG key ID: 9C5E99C8C682FB59
7 changed files with 738 additions and 427 deletions

View file

@ -1,41 +1,22 @@
{-# LANGUAGE OverloadedStrings #-}
module State
( initialState,
prepareView
)
where
-- | The mutable application state's initial value. Navigation logic lives in
-- the UI, which renders chapters lazily into 'stCache'.
module State (initialState) where
import Types (AppState(..), Chapter(chapterTags, chapterTitle), ChapterView(..), EpubEnv, ChapterIndex(..))
import qualified Data.Map.Strict as M
import qualified Data.Text as T
import Types (AppState (..), ChapterIndex (..))
-- Get an element from a list without crashing
safeGet :: ChapterIndex -> [a] -> Maybe a
safeGet (ChapterIndex n) xs
| n < 0 = Nothing
| otherwise = case drop n xs of
(x : _) -> Just x
[] -> Nothing
-- loading template
initialState :: EpubEnv -> AppState
initialState env =
-- | Empty state shown while the book's structure loads in the background.
initialState :: T.Text -> FilePath -> AppState
initialState title path =
AppState
{ cEnv = env,
cIdx = ChapterIndex 0,
cSpine = [],
bTitle = "Svitak - Loading...",
zoomlvl = 1.0,
activeTask = Nothing,
cAllChapters = [],
taskVersion = 0
}
-- prepare the data for the UI
prepareView :: AppState -> ChapterIndex -> Maybe ChapterView
prepareView st index = do
chapter <- safeGet index (cAllChapters st)
pure $ ChapterView
{ viewTitle = bTitle st <> " - " <> chapterTitle chapter
, viewHtml = chapterTags chapter
, viewIdx = index
{ stRefs = [],
stToc = [],
stCache = M.empty,
stIndex = ChapterIndex 0,
stZoom = 1.0,
stTitle = title,
stBookPath = path
}