@@ -28,26 +28,22 @@ 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 )
33import qualified Data.Map as M
32import qualified Data.Map as M
34import qualified Data.Set as Set
33import qualified Data.Set as Set
35import Data.Maybe ( fromMaybe, catMaybes )
34import Data.Maybe ( fromMaybe, catMaybes )
36import Data.List ( isPrefixOf, isInfixOf, intercalate )
35import Data.List ( isPrefixOf, isInfixOf, intercalate )
37−import System.Environment ( getEnv )
38import Text.Printf (printf)
36import Text.Printf (printf)
39import System.FilePath ( takeExtension, takeFileName )
37import System.FilePath ( takeExtension, takeFileName )
40−import System.FilePath.Glob ( namesMatching )
41import Network.HTTP ( urlEncode )
38import Network.HTTP ( urlEncode )
42import qualified Data.ByteString.Lazy as B
39import qualified Data.ByteString.Lazy as B
43import qualified Data.ByteString.Lazy.Char8 as B8
40import qualified Data.ByteString.Lazy.Char8 as B8
44import qualified Text.Pandoc.UTF8 as UTF8
41import qualified Text.Pandoc.UTF8 as UTF8
45import Codec.Archive.Zip ( emptyArchive, addEntryToArchive, eRelativePath, fromEntry , Entry, toEntry, fromArchive)
42import Codec.Archive.Zip ( emptyArchive, addEntryToArchive, eRelativePath, fromEntry , Entry, toEntry, fromArchive)
46−import Data.Time.Clock.POSIX ( getPOSIXTime )
47import Text.Pandoc.Compat.Time
43import Text.Pandoc.Compat.Time
48import Text.Pandoc.Shared ( renderTags', safeRead, uniqueIdent, trim
44import Text.Pandoc.Shared ( renderTags', safeRead, uniqueIdent, trim
49− , normalizeDate, readDataFile, stringify, warn
45+ , normalizeDate, stringify
50− , hierarchicalize, fetchItem' )
46+ , hierarchicalize )
51import qualified Text.Pandoc.Shared as S (Element(..))
47import qualified Text.Pandoc.Shared as S (Element(..))
52import Text.Pandoc.Builder (fromList, setMeta)
48import Text.Pandoc.Builder (fromList, setMeta)
53import Text.Pandoc.Options ( WriterOptions(..)
49import Text.Pandoc.Options ( WriterOptions(..)
@@ -57,18 +53,18 @@ import Text.Pandoc.Options ( WriterOptions(..)
57 , ObfuscationMethod(NoObfuscation) )
53 , ObfuscationMethod(NoObfuscation) )
58import Text.Pandoc.Definition
54import Text.Pandoc.Definition
59import Text.Pandoc.Walk (walk, walkM, query)
55import Text.Pandoc.Walk (walk, walkM, query)
60−import Control.Monad.State (modify, get, State, put, evalState)
56+import Control.Monad.State (modify, get, gets, State, StateT, put, evalState, evalStateT, lift)
61−import Control.Monad (mplus, liftM, when)
57+import Control.Monad (mplus, when)
62import Text.XML.Light ( unode, Element(..), unqual, Attr(..), add_attrs
58import Text.XML.Light ( unode, Element(..), unqual, Attr(..), add_attrs
63 , strContent, lookupAttr, Node(..), QName(..), parseXML
59 , strContent, lookupAttr, Node(..), QName(..), parseXML
64 , onlyElems, node, ppElement)
60 , onlyElems, node, ppElement)
65−import Text.Pandoc.UUID (getRandomUUID)
66import Text.Pandoc.Writers.HTML ( writeHtml )
61import Text.Pandoc.Writers.HTML ( writeHtml )
67import Data.Char ( toLower, isDigit, isAlphaNum )
62import Data.Char ( toLower, isDigit, isAlphaNum )
68import Text.Pandoc.MIME (MimeType, getMimeType, extensionFromMimeType)
63import Text.Pandoc.MIME (MimeType, getMimeType, extensionFromMimeType)
69−import qualified Control.Exception as E
70import Text.Blaze.Html.Renderer.Utf8 (renderHtml)
64import Text.Blaze.Html.Renderer.Utf8 (renderHtml)
71import Text.HTML.TagSoup (Tag(TagOpen), fromAttrib, parseTags)
65import Text.HTML.TagSoup (Tag(TagOpen), fromAttrib, parseTags)
66+import Text.Pandoc.Free (PandocAction, runIO)
67+import qualified Text.Pandoc.Free as P
73-- A Chapter includes a list of blocks and maybe a section
69-- A Chapter includes a list of blocks and maybe a section
74-- number offset. Note, some chapters are unnumbered. The section
70-- number offset. Note, some chapters are unnumbered. The section
@@ -76,6 +72,11 @@ import Text.HTML.TagSoup (Tag(TagOpen), fromAttrib, parseTags)
76-- in filenames, chapter0003.xhtml.
72-- in filenames, chapter0003.xhtml.
77data Chapter = Chapter (Maybe [Int]) [Block]
73data Chapter = Chapter (Maybe [Int]) [Block]
75+data EPUBState = EPUBState { stMediaPaths :: [(FilePath, (FilePath, Maybe Entry))]
78+type E = StateT EPUBState PandocAction
79data EPUBMetadata = EPUBMetadata{
80data EPUBMetadata = EPUBMetadata{
80 epubIdentifier :: [Identifier]
81 epubIdentifier :: [Identifier]
81 , epubTitle :: [Title]
82 , epubTitle :: [Title]
@@ -143,7 +144,7 @@ removeNote :: Inline -> Inline
143removeNote (Note _) = Str ""
144removeNote (Note _) = Str ""
144removeNote x = x
145removeNote x = x
146−getEPUBMetadata :: WriterOptions -> Meta -> IO EPUBMetadata
147+getEPUBMetadata :: WriterOptions -> Meta -> E EPUBMetadata
147getEPUBMetadata opts meta = do
148getEPUBMetadata opts meta = do
148 let md = metadataFromMeta opts meta
149 let md = metadataFromMeta opts meta
149 let elts = onlyElems $ parseXML $ writerEpubMetadata opts
150 let elts = onlyElems $ parseXML $ writerEpubMetadata opts
@@ -151,7 +152,7 @@ getEPUBMetadata opts meta = do
151 let addIdentifier m =
152 let addIdentifier m =
152 if null (epubIdentifier m)
153 if null (epubIdentifier m)
154− randomId <- fmap show getRandomUUID
155+ randomId <- fmap show (lift P.newUUID)
155 return $ m{ epubIdentifier = [Identifier randomId Nothing] }
156 return $ m{ epubIdentifier = [Identifier randomId Nothing] }
156 else return m
157 else return m
157 let addLanguage m =
158 let addLanguage m =
@@ -159,16 +160,19 @@ getEPUBMetadata opts meta = do
159 then case lookup "lang" (writerVariables opts) of
160 then case lookup "lang" (writerVariables opts) of
160 Just x -> return m{ epubLanguage = x }
161 Just x -> return m{ epubLanguage = x }
161 Nothing -> do
162 Nothing -> do
162− localeLang <- E.catch (liftM
163+ mLang <- lift $ P.lookupEnv "LANG"
163− (map (\c -> if c == '_' then '-' else c) .
167+ map (\c -> if c == '_' then '-' else c) $
164− takeWhile (/='.')) $ getEnv "LANG")
168+ takeWhile (/='.') lang
165− (\e -> let _ = (e :: E.SomeException) in return "en-US")
169+ Nothing -> "en-US"
166 return m{ epubLanguage = localeLang }
170 return m{ epubLanguage = localeLang }
167 else return m
171 else return m
168 let fixDate m =
172 let fixDate m =
169 if null (epubDate m)
173 if null (epubDate m)
171− currentTime <- getCurrentTime
175+ currentTime <- lift P.getCurrentTime
172 return $ m{ epubDate = [ Date{
176 return $ m{ epubDate = [ Date{
173 dateText = showDateTimeISO8601 currentTime
177 dateText = showDateTimeISO8601 currentTime
174 , dateEvent = Nothing } ] }
178 , dateEvent = Nothing } ] }
@@ -333,10 +337,24 @@ metadataFromMeta opts meta = EPUBMetadata{
333writeEPUB :: WriterOptions -- ^ Writer options
337writeEPUB :: WriterOptions -- ^ Writer options
334 -> Pandoc -- ^ Document to convert
338 -> Pandoc -- ^ Document to convert
335 -> IO B.ByteString
339 -> IO B.ByteString
336−writeEPUB opts doc@(Pandoc meta _) = do
340+writeEPUB opts doc = runIO $ writeEPUBPure opts doc
342+writeEPUBPure :: WriterOptions -- ^ Writer options
343+ -> Pandoc -- ^ Document to convert
344+ -> PandocAction B.ByteString
345+writeEPUBPure opts doc =
346+ let initState = EPUBState { stMediaPaths = []
349+ evalStateT (pandocToEPUB opts doc) initState
351+pandocToEPUB :: WriterOptions
354+pandocToEPUB opts doc@(Pandoc meta _) = do
337 let version = fromMaybe EPUB2 (writerEpubVersion opts)
355 let version = fromMaybe EPUB2 (writerEpubVersion opts)
338 let epub3 = version == EPUB3
356 let epub3 = version == EPUB3
339− epochtime <- floor `fmap` getPOSIXTime
357+ epochtime <- floor <$> lift P.getPOSIXTime
340 let mkEntry path content = toEntry path epochtime content
358 let mkEntry path content = toEntry path epochtime content
341 let vars = ("epub3", if epub3 then "true" else "false")
359 let vars = ("epub3", if epub3 then "true" else "false")
342 : ("css", "stylesheet.css")
360 : ("css", "stylesheet.css")
@@ -362,7 +380,7 @@ writeEPUB opts doc@(Pandoc meta _) = do
362 let cpContent = renderHtml $ writeHtml
380 let cpContent = renderHtml $ writeHtml
363 opts'{ writerVariables = ("coverpage","true"):vars }
381 opts'{ writerVariables = ("coverpage","true"):vars }
364 (Pandoc meta [RawBlock (Format "html") $ "<div id=\"cover-image\">\n<img src=\"" ++ coverImage ++ "\" alt=\"cover image\" />\n</div>"])
382 (Pandoc meta [RawBlock (Format "html") $ "<div id=\"cover-image\">\n<img src=\"" ++ coverImage ++ "\" alt=\"cover image\" />\n</div>"])
365− imgContent <- B.readFile img
383+ imgContent <- lift $ P.readFileLazy img
366 return ( [mkEntry "cover.xhtml" cpContent]
384 return ( [mkEntry "cover.xhtml" cpContent]
367 , [mkEntry coverImage imgContent] )
385 , [mkEntry coverImage imgContent] )
@@ -373,18 +391,17 @@ writeEPUB opts doc@(Pandoc meta _) = do
373 let tpEntry = mkEntry "title_page.xhtml" tpContent
391 let tpEntry = mkEntry "title_page.xhtml" tpContent
375 -- handle pictures
393 -- handle pictures
376− mediaRef <- newIORef []
394+ -- mediaRef <- P.newIORef []
377− Pandoc _ blocks <- walkM (transformInline opts' mediaRef) doc >>=
395+ Pandoc _ blocks <- walkM (transformInline opts') doc >>=
378− walkM (transformBlock opts' mediaRef)
396+ walkM (transformBlock opts')
379− picEntries <- (catMaybes . map (snd . snd)) <$> readIORef mediaRef
397+ picEntries <- (catMaybes . map (snd . snd)) <$> (gets stMediaPaths)
381 -- handle fonts
398 -- handle fonts
382 let matchingGlob f = do
399 let matchingGlob f = do
383− xs <- namesMatching f
400+ xs <- lift $ P.glob f
384 when (null xs) $
401 when (null xs) $
385− warn $ f ++ " did not match any font files."
402+ lift $ P.warn $ f ++ " did not match any font files."
386 return xs
403 return xs
387− let mkFontEntry f = mkEntry (takeFileName f) `fmap` B.readFile f
404+ let mkFontEntry f = mkEntry (takeFileName f) `fmap` (lift $ P.readFileLazy f)
388 fontFiles <- concat <$> mapM matchingGlob (writerEpubFonts opts')
405 fontFiles <- concat <$> mapM matchingGlob (writerEpubFonts opts')
389 fontEntries <- mapM mkFontEntry fontFiles
406 fontEntries <- mapM mkFontEntry fontFiles
@@ -521,7 +538,7 @@ writeEPUB opts doc@(Pandoc meta _) = do
521 let uuid = case epubIdentifier metadata of
538 let uuid = case epubIdentifier metadata of
522 (x:_) -> identifierText x -- use first identifier as UUID
539 (x:_) -> identifierText x -- use first identifier as UUID
523 [] -> error "epubIdentifier is null" -- shouldn't happen
540 [] -> error "epubIdentifier is null" -- shouldn't happen
524− currentTime <- getCurrentTime
541+ currentTime <- lift $ P.getCurrentTime
525 let contentsData = UTF8.fromStringLazy $ ppTopElement $
542 let contentsData = UTF8.fromStringLazy $ ppTopElement $
526 unode "package" ! [("version", case version of
543 unode "package" ! [("version", case version of
527 EPUB2 -> "2.0"
544 EPUB2 -> "2.0"
@@ -693,10 +710,10 @@ writeEPUB opts doc@(Pandoc meta _) = do
694 -- stylesheet
711 -- stylesheet
695 stylesheet <- case epubStylesheet metadata of
712 stylesheet <- case epubStylesheet metadata of
696− Just (StylesheetPath fp) -> UTF8.readFile fp
713+ Just (StylesheetPath fp) -> lift $ P.readFileUTF8 fp
697 Just (StylesheetContents s) -> return s
714 Just (StylesheetContents s) -> return s
698 Nothing -> UTF8.toString `fmap`
715 Nothing -> UTF8.toString `fmap`
699− readDataFile (writerUserDataDir opts) "epub.css"
716+ (lift $ P.readDataFile (writerUserDataDir opts) "epub.css")
700 let stylesheetEntry = mkEntry "stylesheet.css" $ UTF8.fromStringLazy stylesheet
717 let stylesheetEntry = mkEntry "stylesheet.css" $ UTF8.fromStringLazy stylesheet
702 -- construct archive
719 -- construct archive
@@ -813,78 +830,77 @@ showDateTimeISO8601 :: UTCTime -> String
813showDateTimeISO8601 = formatTime defaultTimeLocale "%FT%TZ"
830showDateTimeISO8601 = formatTime defaultTimeLocale "%FT%TZ"
815transformTag :: WriterOptions
832transformTag :: WriterOptions
816− -> IORef [(FilePath, (FilePath, Maybe Entry))] -- ^ (oldpath, newpath, entry) media
833+ -- -> IORef [(FilePath, (FilePath, Maybe Entry))] -- ^ (oldpath, newpath, entry) media
817 -> Tag String
834 -> Tag String
818− -> IO (Tag String)
835+ -> E (Tag String)
819−transformTag opts mediaRef tag@(TagOpen name attr)
836+transformTag opts tag@(TagOpen name attr)
820 | name `elem` ["video", "source", "img", "audio"] &&
837 | name `elem` ["video", "source", "img", "audio"] &&
821 lookup "data-external" attr == Nothing = do
838 lookup "data-external" attr == Nothing = do
822 let src = fromAttrib "src" tag
839 let src = fromAttrib "src" tag
823 let poster = fromAttrib "poster" tag
840 let poster = fromAttrib "poster" tag
824− newsrc <- modifyMediaRef opts mediaRef src
841+ newsrc <- modifyMediaRef opts src
825− newposter <- modifyMediaRef opts mediaRef poster
842+ newposter <- modifyMediaRef opts poster
826 let attr' = filter (\(x,_) -> x /= "src" && x /= "poster") attr ++
843 let attr' = filter (\(x,_) -> x /= "src" && x /= "poster") attr ++
827 [("src", newsrc) | not (null newsrc)] ++
844 [("src", newsrc) | not (null newsrc)] ++
828 [("poster", newposter) | not (null newposter)]
845 [("poster", newposter) | not (null newposter)]
829 return $ TagOpen name attr'
846 return $ TagOpen name attr'
830−transformTag _ _ tag = return tag
847+transformTag _ tag = return tag
832modifyMediaRef :: WriterOptions
849modifyMediaRef :: WriterOptions
833− -> IORef [(FilePath, (FilePath, Maybe Entry))]
834 -> FilePath
850 -> FilePath
835− -> IO FilePath
851+ -> E FilePath
836−modifyMediaRef _ _ "" = return ""
852+modifyMediaRef _ "" = return ""
837−modifyMediaRef opts mediaRef oldsrc = do
853+modifyMediaRef opts oldsrc = do
838− media <- readIORef mediaRef
854+ media <- gets stMediaPaths
839 case lookup oldsrc media of
855 case lookup oldsrc media of
840 Just (n,_) -> return n
856 Just (n,_) -> return n
841 Nothing -> do
857 Nothing -> do
842− res <- fetchItem' (writerMediaBag opts)
858+ res <- lift $ P.fetchItem' (writerMediaBag opts)
843 (writerSourceURL opts) oldsrc
859 (writerSourceURL opts) oldsrc
844 (new, mbEntry) <-
860 (new, mbEntry) <-
845 case res of
861 case res of
846 Left _ -> do
862 Left _ -> do
847− warn $ "Could not find media `" ++ oldsrc ++ "', skipping..."
863+ lift $ P.warn $ "Could not find media `" ++ oldsrc ++ "', skipping..."
848 return (oldsrc, Nothing)
864 return (oldsrc, Nothing)
849 Right (img,mbMime) -> do
865 Right (img,mbMime) -> do
850 let new = "media/file" ++ show (length media) ++
866 let new = "media/file" ++ show (length media) ++
851 fromMaybe (takeExtension (takeWhile (/='?') oldsrc))
867 fromMaybe (takeExtension (takeWhile (/='?') oldsrc))
852 (('.':) <$> (mbMime >>= extensionFromMimeType))
868 (('.':) <$> (mbMime >>= extensionFromMimeType))
853− epochtime <- floor `fmap` getPOSIXTime
869+ epochtime <- floor `fmap` lift P.getPOSIXTime
854 let entry = toEntry new epochtime $ B.fromChunks . (:[]) $ img
870 let entry = toEntry new epochtime $ B.fromChunks . (:[]) $ img
855 return (new, Just entry)
871 return (new, Just entry)
856− modifyIORef mediaRef ( (oldsrc, (new, mbEntry)): )
872+ modify $ \st -> st{ stMediaPaths = (oldsrc, (new, mbEntry)):media}
857 return new
873 return new
859transformBlock :: WriterOptions
875transformBlock :: WriterOptions
860− -> IORef [(FilePath, (FilePath, Maybe Entry))] -- ^ (oldpath, newpath, entry) media
876+ -- -> IORef [(FilePath, (FilePath, Maybe Entry))] -- ^ (oldpath, newpath, entry) media
862− -> IO Block
878+ -> E Block
863−transformBlock opts mediaRef (RawBlock fmt raw)
879+transformBlock opts (RawBlock fmt raw)
864 | fmt == Format "html" = do
880 | fmt == Format "html" = do
865 let tags = parseTags raw
881 let tags = parseTags raw
866− tags' <- mapM (transformTag opts mediaRef) tags
882+ tags' <- mapM (transformTag opts) tags
867 return $ RawBlock fmt (renderTags' tags')
883 return $ RawBlock fmt (renderTags' tags')
868−transformBlock _ _ b = return b
884+transformBlock _ b = return b
870transformInline :: WriterOptions
886transformInline :: WriterOptions
871− -> IORef [(FilePath, (FilePath, Maybe Entry))] -- ^ (oldpath, newpath) media
887+ -- -> IORef [(FilePath, (FilePath, Maybe Entry))] -- ^ (oldpath, newpath) media
872 -> Inline
888 -> Inline
873− -> IO Inline
889+ -> E Inline
874−transformInline opts mediaRef (Image attr lab (src,tit)) = do
890+transformInline opts (Image attr lab (src,tit)) = do
875− newsrc <- modifyMediaRef opts mediaRef src
891+ newsrc <- modifyMediaRef opts src
876 return $ Image attr lab (newsrc, tit)
892 return $ Image attr lab (newsrc, tit)
877−transformInline opts mediaRef (x@(Math t m))
893+transformInline opts (x@(Math t m))
878 | WebTeX url <- writerHTMLMathMethod opts = do
894 | WebTeX url <- writerHTMLMathMethod opts = do
879− newsrc <- modifyMediaRef opts mediaRef (url ++ urlEncode m)
895+ newsrc <- modifyMediaRef opts (url ++ urlEncode m)
880 let mathclass = if t == DisplayMath then "display" else "inline"
896 let mathclass = if t == DisplayMath then "display" else "inline"
881 return $ Span ("",["math",mathclass],[]) [Image nullAttr [x] (newsrc, "")]
897 return $ Span ("",["math",mathclass],[]) [Image nullAttr [x] (newsrc, "")]
882−transformInline opts mediaRef (RawInline fmt raw)
898+transformInline opts (RawInline fmt raw)
883 | fmt == Format "html" = do
899 | fmt == Format "html" = do
884 let tags = parseTags raw
900 let tags = parseTags raw
885− tags' <- mapM (transformTag opts mediaRef) tags
901+ tags' <- mapM (transformTag opts) tags
886 return $ RawInline fmt (renderTags' tags')
902 return $ RawInline fmt (renderTags' tags')
887−transformInline _ _ x = return x
903+transformInline _ x = return x
889(!) :: (t -> Element) -> [(String, String)] -> t -> Element
905(!) :: (t -> Element) -> [(String, String)] -> t -> Element
890(!) f attrs n = add_attrs (map (\(k,v) -> Attr (unqual k) v) attrs) (f n)
906(!) f attrs n = add_attrs (map (\(k,v) -> Attr (unqual k) v) attrs) (f n)