98 lines
3.4 KiB
Haskell
98 lines
3.4 KiB
Haskell
module EpubParser where
|
|
|
|
import Codec.Archive.Zip (toArchive, findEntryByPath, fromEntry, Archive)
|
|
import qualified Data.ByteString.Lazy as BL
|
|
import qualified Data.ByteString.Lazy.Char8 as C8
|
|
import Text.HTML.TagSoup (parseTags, Tag)
|
|
import Text.HTML.Scalpel (scrape, text, tagSelector, attr, attrs, Scraper, chroots, anySelector)
|
|
import System.FilePath (takeDirectory, (</>))
|
|
import Control.Monad.Reader
|
|
import qualified Data.Text as T
|
|
import Data.Maybe (fromMaybe, catMaybes)
|
|
import qualified Data.Map as M
|
|
|
|
type EpubAction a = ReaderT EpubEnv IO a
|
|
|
|
data EpubEnv = EpubEnv
|
|
{ title :: T.Text
|
|
, author :: T.Text
|
|
, rootPrefix :: FilePath
|
|
, spine :: [T.Text]
|
|
, manifestMap :: M.Map T.Text T.Text
|
|
, archive :: Archive
|
|
} deriving (Show)
|
|
|
|
myEnv :: Archive -> [Tag String] -> EpubEnv
|
|
myEnv arch tags =
|
|
let
|
|
opfPath = fromMaybe "" (getOpfPath arch)
|
|
prefix = getRootPrefix opfPath
|
|
|
|
in
|
|
EpubEnv
|
|
{ title = getTagText "dc:title" tags
|
|
, author = getTagText "dc:creator" tags
|
|
, rootPrefix = prefix
|
|
, spine = getSpine tags
|
|
, manifestMap = getManifest tags
|
|
, archive = arch
|
|
}
|
|
|
|
openEpub :: FilePath -> IO (Either String Archive)
|
|
openEpub path = toArchive <$> BL.readFile path >>= \archive -> maybe (pure $ Left "Error reading file") (\entry -> if fromEntry entry == C8.pack "application/epub+zip" then pure (Right archive) else pure (Left "Wrong mimetype")) (findEntryByPath "mimetype" archive)
|
|
|
|
getOpfPath :: Archive -> Maybe FilePath
|
|
getOpfPath archive = do
|
|
entry <- findEntryByPath "META-INF/container.xml" archive
|
|
let tags = parseTags $ C8.unpack (fromEntry entry)
|
|
scrape (attr "full-path" (tagSelector "rootfile")) tags
|
|
|
|
getOpfTags :: Archive -> FilePath -> [Tag String]
|
|
getOpfTags archive opfpath =
|
|
case findEntryByPath opfpath archive of
|
|
Just entry -> parseTags $ C8.unpack (fromEntry entry)
|
|
Nothing -> []
|
|
|
|
-- Makes it easier so we can just use this function and concatenate relative paths
|
|
getRootPrefix :: FilePath -> FilePath
|
|
getRootPrefix path =
|
|
let dir = takeDirectory path
|
|
in if dir == "." then "" else dir ++ "/"
|
|
|
|
getTagText :: String -> [Tag String] -> T.Text
|
|
getTagText tagName tags =
|
|
let scraper = text (tagSelector tagName)
|
|
in T.pack $ fromMaybe "" (scrape scraper tags)
|
|
|
|
getSpine :: [Tag String] -> [T.Text]
|
|
getSpine tags =
|
|
map T.pack . fromMaybe [] $ scrape (attrs "idref" (tagSelector "itemref")) tags
|
|
|
|
-- This scraper will be run by chroot for every item
|
|
itemScraper :: Scraper String (T.Text, T.Text)
|
|
itemScraper =
|
|
liftA2 (,) (attr "id" anySelector) (attr "href" anySelector) >>= \(id, href) -> pure (T.pack id, T.pack href)
|
|
|
|
getManifest :: [Tag String] -> M.Map T.Text T.Text
|
|
getManifest tags =
|
|
M.fromList . fromMaybe [] $ scrape (chroots (tagSelector "item") itemScraper) tags
|
|
|
|
resolvePath :: T.Text -> EpubAction (Maybe FilePath)
|
|
resolvePath spineId = do
|
|
m <- asks manifestMap
|
|
p <- asks rootPrefix
|
|
|
|
pure $ fmap (\href -> p </> T.unpack href) (M.lookup spineId m)
|
|
|
|
resolveSpine :: EpubAction [FilePath]
|
|
resolveSpine = catMaybes <$> (asks spine >>= mapM resolvePath)
|
|
|
|
getChapter :: FilePath -> EpubAction T.Text
|
|
getChapter path = do
|
|
arch <- asks archive
|
|
case findEntryByPath path arch of
|
|
Nothing -> pure $ T.pack "Error: Chapter not found."
|
|
Just entry -> do
|
|
let content = C8.unpack (fromEntry entry)
|
|
let rawText = scrape (text anySelector) (parseTags content)
|
|
pure $ T.pack $ fromMaybe "" rawText
|