@@ -28,26 +28,23 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
29Conversion of 'Pandoc' documents to EPUB.
29Conversion of 'Pandoc' documents to EPUB.
31−module Text.Pandoc.Writers.EPUB ( writeEPUB ) where
31+module Text.Pandoc.Writers.EPUB ( writeEPUB, writeEPUBPure ) where
32−import Data.IORef ( IORef, newIORef, readIORef, modifyIORef )
32+import Data.IORef ( IORef )
33import qualified Data.Map as M
33import qualified Data.Map as M
34import qualified Data.Set as Set
34import qualified Data.Set as Set
35import Data.Maybe ( fromMaybe, catMaybes )
35import Data.Maybe ( fromMaybe, catMaybes )
36import Data.List ( isPrefixOf, isInfixOf, intercalate )
36import Data.List ( isPrefixOf, isInfixOf, intercalate )
37−import System.Environment ( getEnv )
38import Text.Printf (printf)
37import Text.Printf (printf)
39import System.FilePath ( takeExtension, takeFileName )
38import System.FilePath ( takeExtension, takeFileName )
40−import System.FilePath.Glob ( namesMatching )
41import Network.HTTP ( urlEncode )
39import Network.HTTP ( urlEncode )
42import qualified Data.ByteString.Lazy as B
40import qualified Data.ByteString.Lazy as B
43import qualified Data.ByteString.Lazy.Char8 as B8
41import qualified Data.ByteString.Lazy.Char8 as B8
44import qualified Text.Pandoc.UTF8 as UTF8
42import qualified Text.Pandoc.UTF8 as UTF8
45import Codec.Archive.Zip ( emptyArchive, addEntryToArchive, eRelativePath, fromEntry , Entry, toEntry, fromArchive)
43import Codec.Archive.Zip ( emptyArchive, addEntryToArchive, eRelativePath, fromEntry , Entry, toEntry, fromArchive)
46−import Data.Time.Clock.POSIX ( getPOSIXTime )
47import Text.Pandoc.Compat.Time
44import Text.Pandoc.Compat.Time
48import Text.Pandoc.Shared ( renderTags', safeRead, uniqueIdent, trim
45import Text.Pandoc.Shared ( renderTags', safeRead, uniqueIdent, trim
49− , normalizeDate, readDataFile, stringify, warn
46+ , normalizeDate, stringify
50− , hierarchicalize, fetchItem' )
47+ , hierarchicalize )
51import qualified Text.Pandoc.Shared as S (Element(..))
48import qualified Text.Pandoc.Shared as S (Element(..))
52import Text.Pandoc.Builder (fromList, setMeta)
49import Text.Pandoc.Builder (fromList, setMeta)
53import Text.Pandoc.Options ( WriterOptions(..)
50import Text.Pandoc.Options ( WriterOptions(..)
@@ -58,17 +55,19 @@ import Text.Pandoc.Options ( WriterOptions(..)
58import Text.Pandoc.Definition
55import Text.Pandoc.Definition
59import Text.Pandoc.Walk (walk, walkM, query)
56import Text.Pandoc.Walk (walk, walkM, query)
60import Control.Monad.State (modify, get, State, put, evalState)
57import Control.Monad.State (modify, get, State, put, evalState)
61−import Control.Monad (mplus, liftM, when)
58+import Control.Monad (mplus, when)
62import Text.XML.Light ( unode, Element(..), unqual, Attr(..), add_attrs
59import Text.XML.Light ( unode, Element(..), unqual, Attr(..), add_attrs
63 , strContent, lookupAttr, Node(..), QName(..), parseXML
60 , strContent, lookupAttr, Node(..), QName(..), parseXML
64 , onlyElems, node, ppElement)
61 , onlyElems, node, ppElement)
65−import Text.Pandoc.UUID (getRandomUUID)
66import Text.Pandoc.Writers.HTML ( writeHtml )
62import Text.Pandoc.Writers.HTML ( writeHtml )
67import Data.Char ( toLower, isDigit, isAlphaNum )
63import Data.Char ( toLower, isDigit, isAlphaNum )
68import Text.Pandoc.MIME (MimeType, getMimeType, extensionFromMimeType)
64import Text.Pandoc.MIME (MimeType, getMimeType, extensionFromMimeType)
69−import qualified Control.Exception as E
70import Text.Blaze.Html.Renderer.Utf8 (renderHtml)
65import Text.Blaze.Html.Renderer.Utf8 (renderHtml)
71import Text.HTML.TagSoup (Tag(TagOpen), fromAttrib, parseTags)
66import Text.HTML.TagSoup (Tag(TagOpen), fromAttrib, parseTags)
67+import Text.Pandoc.Free (PandocAction, runIO)
68+import qualified Text.Pandoc.Free as P
70+type EPUBAction = PandocAction [(FilePath, (FilePath, Maybe Entry))]
73-- A Chapter includes a list of blocks and maybe a section
72-- A Chapter includes a list of blocks and maybe a section
74-- number offset. Note, some chapters are unnumbered. The section
73-- number offset. Note, some chapters are unnumbered. The section
@@ -143,7 +142,7 @@ removeNote :: Inline -> Inline
143removeNote (Note _) = Str ""
142removeNote (Note _) = Str ""
144removeNote x = x
143removeNote x = x
146−getEPUBMetadata :: WriterOptions -> Meta -> IO EPUBMetadata
145+getEPUBMetadata :: WriterOptions -> Meta -> EPUBAction EPUBMetadata
147getEPUBMetadata opts meta = do
146getEPUBMetadata opts meta = do
148 let md = metadataFromMeta opts meta
147 let md = metadataFromMeta opts meta
149 let elts = onlyElems $ parseXML $ writerEpubMetadata opts
148 let elts = onlyElems $ parseXML $ writerEpubMetadata opts
@@ -151,7 +150,7 @@ getEPUBMetadata opts meta = do
151 let addIdentifier m =
150 let addIdentifier m =
152 if null (epubIdentifier m)
151 if null (epubIdentifier m)
154− randomId <- fmap show getRandomUUID
153+ randomId <- fmap show P.newUUID
155 return $ m{ epubIdentifier = [Identifier randomId Nothing] }
154 return $ m{ epubIdentifier = [Identifier randomId Nothing] }
156 else return m
155 else return m
157 let addLanguage m =
156 let addLanguage m =
@@ -159,16 +158,19 @@ getEPUBMetadata opts meta = do
159 then case lookup "lang" (writerVariables opts) of
158 then case lookup "lang" (writerVariables opts) of
160 Just x -> return m{ epubLanguage = x }
159 Just x -> return m{ epubLanguage = x }
161 Nothing -> do
160 Nothing -> do
162− localeLang <- E.catch (liftM
161+ mLang <- P.lookupEnv "LANG"
163− (map (\c -> if c == '_' then '-' else c) .
165+ map (\c -> if c == '_' then '-' else c) $
164− takeWhile (/='.')) $ getEnv "LANG")
166+ takeWhile (/='.') lang
165− (\e -> let _ = (e :: E.SomeException) in return "en-US")
167+ Nothing -> "en-US"
166 return m{ epubLanguage = localeLang }
168 return m{ epubLanguage = localeLang }
167 else return m
169 else return m
168 let fixDate m =
170 let fixDate m =
169 if null (epubDate m)
171 if null (epubDate m)
171− currentTime <- getCurrentTime
173+ currentTime <- P.getCurrentTime
172 return $ m{ epubDate = [ Date{
174 return $ m{ epubDate = [ Date{
173 dateText = showDateTimeISO8601 currentTime
175 dateText = showDateTimeISO8601 currentTime
174 , dateEvent = Nothing } ] }
176 , dateEvent = Nothing } ] }
@@ -333,10 +335,15 @@ metadataFromMeta opts meta = EPUBMetadata{
333writeEPUB :: WriterOptions -- ^ Writer options
335writeEPUB :: WriterOptions -- ^ Writer options
334 -> Pandoc -- ^ Document to convert
336 -> Pandoc -- ^ Document to convert
335 -> IO B.ByteString
337 -> IO B.ByteString
336−writeEPUB opts doc@(Pandoc meta _) = do
338+writeEPUB opts doc = runIO $ writeEPUBPure opts doc
340+writeEPUBPure :: WriterOptions -- ^ Writer options
341+ -> Pandoc -- ^ Document to convert
342+ -> EPUBAction B.ByteString
343+writeEPUBPure opts doc@(Pandoc meta _) = do
337 let version = fromMaybe EPUB2 (writerEpubVersion opts)
344 let version = fromMaybe EPUB2 (writerEpubVersion opts)
338 let epub3 = version == EPUB3
345 let epub3 = version == EPUB3
339− epochtime <- floor `fmap` getPOSIXTime
346+ epochtime <- floor <$> P.getPOSIXTime
340 let mkEntry path content = toEntry path epochtime content
347 let mkEntry path content = toEntry path epochtime content
341 let vars = ("epub3", if epub3 then "true" else "false")
348 let vars = ("epub3", if epub3 then "true" else "false")
342 : ("css", "stylesheet.css")
349 : ("css", "stylesheet.css")
@@ -361,7 +368,7 @@ writeEPUB opts doc@(Pandoc meta _) = do
361 let cpContent = renderHtml $ writeHtml
368 let cpContent = renderHtml $ writeHtml
362 opts'{ writerVariables = ("coverpage","true"):vars }
369 opts'{ writerVariables = ("coverpage","true"):vars }
363 (Pandoc meta [RawBlock (Format "html") $ "<div id=\"cover-image\">\n<img src=\"" ++ coverImage ++ "\" alt=\"cover image\" />\n</div>"])
370 (Pandoc meta [RawBlock (Format "html") $ "<div id=\"cover-image\">\n<img src=\"" ++ coverImage ++ "\" alt=\"cover image\" />\n</div>"])
364− imgContent <- B.readFile img
371+ imgContent <- P.readFileLazy img
365 return ( [mkEntry "cover.xhtml" cpContent]
372 return ( [mkEntry "cover.xhtml" cpContent]
366 , [mkEntry coverImage imgContent] )
373 , [mkEntry coverImage imgContent] )
@@ -372,18 +379,18 @@ writeEPUB opts doc@(Pandoc meta _) = do
372 let tpEntry = mkEntry "title_page.xhtml" tpContent
379 let tpEntry = mkEntry "title_page.xhtml" tpContent
374 -- handle pictures
381 -- handle pictures
375− mediaRef <- newIORef []
382+ mediaRef <- P.newIORef []
376 Pandoc _ blocks <- walkM (transformInline opts' mediaRef) doc >>=
383 Pandoc _ blocks <- walkM (transformInline opts' mediaRef) doc >>=
377 walkM (transformBlock opts' mediaRef)
384 walkM (transformBlock opts' mediaRef)
378− picEntries <- (catMaybes . map (snd . snd)) <$> readIORef mediaRef
385+ picEntries <- (catMaybes . map (snd . snd)) <$> P.readIORef mediaRef
380 -- handle fonts
387 -- handle fonts
381 let matchingGlob f = do
388 let matchingGlob f = do
382− xs <- namesMatching f
389+ xs <- P.namesMatching f
383 when (null xs) $
390 when (null xs) $
384− warn $ f ++ " did not match any font files."
391+ P.warn $ f ++ " did not match any font files."
385 return xs
392 return xs
386− let mkFontEntry f = mkEntry (takeFileName f) `fmap` B.readFile f
393+ let mkFontEntry f = mkEntry (takeFileName f) `fmap` P.readFileLazy f
387 fontFiles <- concat <$> mapM matchingGlob (writerEpubFonts opts')
394 fontFiles <- concat <$> mapM matchingGlob (writerEpubFonts opts')
388 fontEntries <- mapM mkFontEntry fontFiles
395 fontEntries <- mapM mkFontEntry fontFiles
@@ -520,7 +527,7 @@ writeEPUB opts doc@(Pandoc meta _) = do
520 let uuid = case epubIdentifier metadata of
527 let uuid = case epubIdentifier metadata of
521 (x:_) -> identifierText x -- use first identifier as UUID
528 (x:_) -> identifierText x -- use first identifier as UUID
522 [] -> error "epubIdentifier is null" -- shouldn't happen
529 [] -> error "epubIdentifier is null" -- shouldn't happen
523− currentTime <- getCurrentTime
530+ currentTime <- P.getCurrentTime
524 let contentsData = UTF8.fromStringLazy $ ppTopElement $
531 let contentsData = UTF8.fromStringLazy $ ppTopElement $
525 unode "package" ! [("version", case version of
532 unode "package" ! [("version", case version of
526 EPUB2 -> "2.0"
533 EPUB2 -> "2.0"
@@ -692,10 +699,10 @@ writeEPUB opts doc@(Pandoc meta _) = do
693 -- stylesheet
700 -- stylesheet
694 stylesheet <- case epubStylesheet metadata of
701 stylesheet <- case epubStylesheet metadata of
695− Just (StylesheetPath fp) -> UTF8.readFile fp
702+ Just (StylesheetPath fp) -> P.readFileUTF8 fp
696 Just (StylesheetContents s) -> return s
703 Just (StylesheetContents s) -> return s
697 Nothing -> UTF8.toString `fmap`
704 Nothing -> UTF8.toString `fmap`
698− readDataFile (writerUserDataDir opts) "epub.css"
705+ P.readDataFile (writerUserDataDir opts) "epub.css"
699 let stylesheetEntry = mkEntry "stylesheet.css" $ UTF8.fromStringLazy stylesheet
706 let stylesheetEntry = mkEntry "stylesheet.css" $ UTF8.fromStringLazy stylesheet
701 -- construct archive
708 -- construct archive
@@ -814,7 +821,7 @@ showDateTimeISO8601 = formatTime defaultTimeLocale "%FT%TZ"
814transformTag :: WriterOptions
821transformTag :: WriterOptions
815 -> IORef [(FilePath, (FilePath, Maybe Entry))] -- ^ (oldpath, newpath, entry) media
822 -> IORef [(FilePath, (FilePath, Maybe Entry))] -- ^ (oldpath, newpath, entry) media
816 -> Tag String
823 -> Tag String
817− -> IO (Tag String)
824+ -> EPUBAction (Tag String)
818transformTag opts mediaRef tag@(TagOpen name attr)
825transformTag opts mediaRef tag@(TagOpen name attr)
819 | name `elem` ["video", "source", "img", "audio"] &&
826 | name `elem` ["video", "source", "img", "audio"] &&
820 lookup "data-external" attr == Nothing = do
827 lookup "data-external" attr == Nothing = do
@@ -831,34 +838,34 @@ transformTag _ _ tag = return tag
831modifyMediaRef :: WriterOptions
838modifyMediaRef :: WriterOptions
832 -> IORef [(FilePath, (FilePath, Maybe Entry))]
839 -> IORef [(FilePath, (FilePath, Maybe Entry))]
833 -> FilePath
840 -> FilePath
834− -> IO FilePath
841+ -> EPUBAction FilePath
835modifyMediaRef _ _ "" = return ""
842modifyMediaRef _ _ "" = return ""
836modifyMediaRef opts mediaRef oldsrc = do
843modifyMediaRef opts mediaRef oldsrc = do
837− media <- readIORef mediaRef
844+ media <- P.readIORef mediaRef
838 case lookup oldsrc media of
845 case lookup oldsrc media of
839 Just (n,_) -> return n
846 Just (n,_) -> return n
840 Nothing -> do
847 Nothing -> do
841− res <- fetchItem' (writerMediaBag opts)
848+ res <- P.fetchItem' (writerMediaBag opts)
842 (writerSourceURL opts) oldsrc
849 (writerSourceURL opts) oldsrc
843 (new, mbEntry) <-
850 (new, mbEntry) <-
844 case res of
851 case res of
845 Left _ -> do
852 Left _ -> do
846− warn $ "Could not find media `" ++ oldsrc ++ "', skipping..."
853+ P.warn $ "Could not find media `" ++ oldsrc ++ "', skipping..."
847 return (oldsrc, Nothing)
854 return (oldsrc, Nothing)
848 Right (img,mbMime) -> do
855 Right (img,mbMime) -> do
849 let new = "media/file" ++ show (length media) ++
856 let new = "media/file" ++ show (length media) ++
850 fromMaybe (takeExtension (takeWhile (/='?') oldsrc))
857 fromMaybe (takeExtension (takeWhile (/='?') oldsrc))
851 (('.':) <$> (mbMime >>= extensionFromMimeType))
858 (('.':) <$> (mbMime >>= extensionFromMimeType))
852− epochtime <- floor `fmap` getPOSIXTime
859+ epochtime <- floor `fmap` P.getPOSIXTime
853 let entry = toEntry new epochtime $ B.fromChunks . (:[]) $ img
860 let entry = toEntry new epochtime $ B.fromChunks . (:[]) $ img
854 return (new, Just entry)
861 return (new, Just entry)
855− modifyIORef mediaRef ( (oldsrc, (new, mbEntry)): )
862+ P.modifyIORef mediaRef ( (oldsrc, (new, mbEntry)): )
856 return new
863 return new
858transformBlock :: WriterOptions
865transformBlock :: WriterOptions
859 -> IORef [(FilePath, (FilePath, Maybe Entry))] -- ^ (oldpath, newpath, entry) media
866 -> IORef [(FilePath, (FilePath, Maybe Entry))] -- ^ (oldpath, newpath, entry) media
861− -> IO Block
868+ -> EPUBAction Block
862transformBlock opts mediaRef (RawBlock fmt raw)
869transformBlock opts mediaRef (RawBlock fmt raw)
863 | fmt == Format "html" = do
870 | fmt == Format "html" = do
864 let tags = parseTags raw
871 let tags = parseTags raw
@@ -869,7 +876,7 @@ transformBlock _ _ b = return b
869transformInline :: WriterOptions
876transformInline :: WriterOptions
870 -> IORef [(FilePath, (FilePath, Maybe Entry))] -- ^ (oldpath, newpath) media
877 -> IORef [(FilePath, (FilePath, Maybe Entry))] -- ^ (oldpath, newpath) media
871 -> Inline
878 -> Inline
872− -> IO Inline
879+ -> EPUBAction Inline
873transformInline opts mediaRef (Image attr lab (src,tit)) = do
880transformInline opts mediaRef (Image attr lab (src,tit)) = do
874 newsrc <- modifyMediaRef opts mediaRef src
881 newsrc <- modifyMediaRef opts mediaRef src
875 return $ Image attr lab (newsrc, tit)
882 return $ Image attr lab (newsrc, tit)