svitak/app/Navigation.hs
Marko Andjelic 2011d638b0
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.
2026-06-26 03:45:29 +01:00

71 lines
2.4 KiB
Haskell

{-# LANGUAGE OverloadedStrings #-}
-- | Scrapers that turn a table-of-contents document into a /tree/ of raw
-- entries. Two formats are supported:
--
-- * EPUB 2 — an NCX file with nested @\<navPoint\>@s.
-- * EPUB 3 — a nav document with nested @\<ol\>@\/@\<li\>@ lists.
--
-- Hrefs are left relative here; 'EpubParser' resolves them against the TOC
-- document's location. We use 'atDepth' so each recursion level only picks up
-- its /direct/ children (otherwise scalpel's descendant matching would flatten
-- the hierarchy and duplicate nested nodes).
module Navigation
( RawToc (..),
navDocHref,
ncxToc,
navToc,
guideToc,
)
where
import Control.Applicative ((<|>))
import qualified Data.Text as T
import Text.HTML.Scalpel (Scraper, atDepth, attr, chroot, chroots, scrapeStringLike, text, (@:), (@=))
-- | A TOC node with an as-yet-unresolved (document-relative) href.
data RawToc = RawToc
{ rawLabel :: T.Text,
rawHref :: FilePath,
rawChildren :: [RawToc]
}
deriving (Show)
-- | The EPUB 3 navigation document's href, declared in the OPF manifest as the
-- item carrying @properties="nav"@.
navDocHref :: String -> Maybe FilePath
navDocHref opf =
scrapeStringLike opf (attr "href" ("item" @: ["properties" @= "nav"]))
-- | EPUB 2: walk the @\<navMap\>@ of nested @\<navPoint\>@s.
ncxToc :: Scraper T.Text [RawToc]
ncxToc = chroot "navMap" points
where
points = chroots ("navPoint" `atDepth` 1) node
node =
RawToc
<$> (T.strip <$> text ("navLabel" `atDepth` 1))
<*> (T.unpack <$> attr "src" ("content" `atDepth` 1))
<*> points
-- | EPUB 3: walk @\<nav epub:type="toc"\>@'s nested @\<ol\>@\/@\<li\>@ lists.
navToc :: Scraper T.Text [RawToc]
navToc = chroot ("nav" @: ["epub:type" @= "toc"]) (chroot "ol" items)
where
items = chroots ("li" `atDepth` 1) node
node =
RawToc
<$> (T.strip <$> text ("a" `atDepth` 1))
<*> (T.unpack <$> attr "href" ("a" `atDepth` 1))
<*> (chroot ("ol" `atDepth` 1) items <|> pure [])
-- | EPUB 2 @\<guide\>@: a flat list of @\<reference\>@s (cover, toc, start, …).
-- Used as a last-resort TOC when there's no NCX or nav document.
guideToc :: Scraper T.Text [RawToc]
guideToc = chroot "guide" $ chroots "reference" node
where
node =
RawToc
<$> (T.strip <$> attr "title" "reference")
<*> (T.unpack <$> attr "href" "reference")
<*> pure []