git.iko.soy/typeable/pandoc
on 7f4348f help sign in
9 files changed+578−233
since splitsnapshots
unifiedsplit
pandoc.cabal+3−2 src/Text/Pandoc/Free.hs+297 src/Text/Pandoc/UUID.hs+12−11 src/Text/Pandoc/Writers/Docx.hs+37−28 src/Text/Pandoc/Writers/EPUB.hs+78−61 src/Text/Pandoc/Writers/FB2.hs+82−95 src/Text/Pandoc/Writers/ICML.hs+11−6 src/Text/Pandoc/Writers/ODT.hs+45−26 src/Text/Pandoc/Writers/RTF.hs+13−4
pandoc.cabal+3−2
@@ -291,10 +291,10 @@ Library
291291 old-time,
292292 deepseq >= 1.3 && < 1.5,
293293 JuicyPixels >= 3.1.6.1 && < 3.3,
294− filemanip >= 0.3 && < 0.4,
294+ Glob >= 0.7 && < 0.8,
295295 cmark >= 0.5 && < 0.6,
296296 doctemplates >= 0.1 && < 0.2,
297− ghc-prim >= 0.2
297+ free >= 4
298298 if flag(old-locale)
299299 Build-Depends: old-locale >= 1 && < 1.1,
300300 time >= 1.2 && < 1.5
@@ -385,6 +385,7 @@ Library
385385 Text.Pandoc.SelfContained,
386386 Text.Pandoc.Process,
387387 Text.Pandoc.CSS
388+ Text.Pandoc.Free
388389 Other-Modules: Text.Pandoc.Readers.Docx.Lists,
389390 Text.Pandoc.Readers.Docx.Combine,
390391 Text.Pandoc.Readers.Docx.Parse,
src/Text/Pandoc/Free.hsnew file+297
@@ -0,0 +1,297 @@
1+{-# LANGUAGE DeriveFunctor, DeriveDataTypeable #-}
2+
3+{-
4+Copyright (C) 2016 Jesse Rosenthal <jrosenthal@jhu.edu>
5+
6+This program is free software; you can redistribute it and/or modify
7+it under the terms of the GNU General Public License as published by
8+the Free Software Foundation; either version 2 of the License, or
9+(at your option) any later version.
10+
11+This program is distributed in the hope that it will be useful,
12+but WITHOUT ANY WARRANTY; without even the implied warranty of
13+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+GNU General Public License for more details.
15+
16+You should have received a copy of the GNU General Public License
17+along with this program; if not, write to the Free Software
18+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19+-}
20+
21+{- |
22+ Module : Text.Pandoc.Free
23+ Copyright : Copyright (C) 2016 Jesse Rosenthal
24+ License : GNU GPL, version 2 or above
25+
26+ Maintainer : Jesse Rosenthal <jrosenthal@jhu.edu>
27+ Stability : alpha
28+ Portability : portable
29+
30+Pure implementations of the IO monads used in Pandoc's readers and writers.
31+-}
32+
33+module Text.Pandoc.Free ( PandocActionF(..)
34+ , PandocAction
35+ , runIO
36+ , runTest
37+ , TestState(..)
38+ , TestEnv(..)
39+ , liftF
40+ --
41+ , lookupEnv
42+ , getCurrentTime
43+ , getPOSIXTime
44+ , getDefaultReferenceDocx
45+ , getDefaultReferenceODT
46+ , newStdGen
47+ , newUniqueHash
48+ , readFileStrict
49+ , readFileLazy
50+ , readFileUTF8
51+ , readDataFile
52+ , fetchItem
53+ , fetchItem'
54+ , warn
55+ , fail
56+ , glob
57+ ) where
58+
59+import Prelude hiding (readFile, fail)
60+import qualified Control.Monad as M (fail)
61+import System.Random (StdGen, next)
62+import qualified System.Random as IO (newStdGen)
63+import Codec.Archive.Zip (Archive, fromArchive)
64+import Data.Unique (hashUnique)
65+import qualified Data.Unique as IO (newUnique)
66+import qualified Text.Pandoc.Shared as IO ( fetchItem
67+ , fetchItem'
68+ , getDefaultReferenceDocx
69+ , getDefaultReferenceODT
70+ , warn
71+ , readDataFile)
72+import Text.Pandoc.MediaBag (MediaBag, lookupMedia)
73+import Data.Time.Clock.POSIX (POSIXTime, utcTimeToPOSIXSeconds)
74+import qualified Data.Time.Clock.POSIX as IO (getPOSIXTime)
75+import Text.Pandoc.Compat.Time (UTCTime)
76+import qualified Text.Pandoc.Compat.Time as IO (getCurrentTime)
77+import Text.Pandoc.MIME (MimeType, getMimeType)
78+import qualified Data.ByteString as B
79+import qualified Data.ByteString.Lazy as BL
80+import Control.Monad.Free
81+import qualified Control.Exception as E
82+import qualified System.Environment as IO (lookupEnv)
83+import qualified Text.Pandoc.UTF8 as UTF8 (readFile, toString)
84+import System.FilePath.Glob (match, compile)
85+import System.FilePath ((</>))
86+import qualified System.FilePath.Glob as IO (glob)
87+import Control.Monad.State hiding (fail)
88+import Control.Monad.Reader hiding (fail)
89+import Data.Word (Word8)
90+import Data.Typeable
91+
92+data PandocActionF nxt =
93+ LookupEnv String (Maybe String -> nxt)
94+ | GetCurrentTime (UTCTime -> nxt)
95+ | GetPOSIXTime (POSIXTime -> nxt)
96+ | GetDefaultReferenceDocx (Maybe FilePath) (Archive -> nxt)
97+ | GetDefaultReferenceODT (Maybe FilePath) (Archive -> nxt)
98+ | NewStdGen (StdGen -> nxt)
99+ | NewUniqueHash (Int -> nxt)
100+ | ReadFileStrict FilePath (B.ByteString -> nxt)
101+ | ReadFileLazy FilePath (BL.ByteString -> nxt)
102+ | ReadFileUTF8 FilePath (String -> nxt)
103+ | ReadDataFile (Maybe FilePath) FilePath (B.ByteString -> nxt)
104+ | FetchItem (Maybe String) (String)
105+ (Either E.SomeException (B.ByteString, Maybe MimeType) -> nxt)
106+ | FetchItem' MediaBag (Maybe String) (String)
107+ (Either E.SomeException (B.ByteString, Maybe MimeType) -> nxt)
108+ | Glob String ([FilePath] -> nxt)
109+ | Warn String nxt
110+ | Fail String
111+ deriving Functor
112+
113+type PandocAction = Free PandocActionF
114+
115+lookupEnv :: String -> PandocAction (Maybe String)
116+lookupEnv s = liftF $ LookupEnv s id
117+
118+getCurrentTime :: PandocAction UTCTime
119+getCurrentTime = liftF $ GetCurrentTime id
120+
121+getPOSIXTime :: PandocAction POSIXTime
122+getPOSIXTime = liftF $ GetPOSIXTime id
123+
124+getDefaultReferenceDocx :: Maybe FilePath -> PandocAction Archive
125+getDefaultReferenceDocx fp = liftF $ GetDefaultReferenceDocx fp id
126+
127+getDefaultReferenceODT :: Maybe FilePath -> PandocAction Archive
128+getDefaultReferenceODT fp = liftF $ GetDefaultReferenceODT fp id
129+
130+newStdGen :: PandocAction StdGen
131+newStdGen = liftF $ NewStdGen id
132+
133+newUniqueHash :: PandocAction Int
134+newUniqueHash = liftF $ NewUniqueHash id
135+
136+readFileStrict :: FilePath -> PandocAction B.ByteString
137+readFileStrict fp = liftF $ ReadFileStrict fp id
138+
139+readFileLazy :: FilePath -> PandocAction BL.ByteString
140+readFileLazy fp = liftF $ ReadFileLazy fp id
141+
142+readFileUTF8 :: FilePath -> PandocAction String
143+readFileUTF8 fp = liftF $ ReadFileUTF8 fp id
144+
145+readDataFile :: Maybe FilePath -> FilePath -> PandocAction B.ByteString
146+readDataFile mfp fp = liftF $ ReadDataFile mfp fp id
147+
148+fetchItem :: Maybe String ->
149+ String ->
150+ PandocAction (Either E.SomeException (B.ByteString, Maybe MimeType))
151+fetchItem ms s = liftF $ FetchItem ms s id
152+
153+
154+fetchItem' :: MediaBag ->
155+ Maybe String ->
156+ String ->
157+ PandocAction (Either E.SomeException (B.ByteString, Maybe MimeType))
158+fetchItem' mb ms s = liftF $ FetchItem' mb ms s id
159+
160+warn :: String -> PandocAction ()
161+warn s = liftF $ Warn s ()
162+
163+fail :: String -> PandocAction b
164+fail s = liftF $ Fail s
165+
166+glob :: String -> PandocAction [FilePath]
167+glob s = liftF $ Glob s id
168+
169+runIO :: PandocAction nxt -> IO nxt
170+runIO (Free (LookupEnv s f)) = IO.lookupEnv s >>= runIO . f
171+runIO (Free (GetCurrentTime f)) = IO.getCurrentTime >>= runIO . f
172+runIO (Free (GetPOSIXTime f)) = IO.getPOSIXTime >>= runIO . f
173+runIO (Free (GetDefaultReferenceDocx mfp f)) =
174+ IO.getDefaultReferenceDocx mfp >>= runIO . f
175+runIO (Free (GetDefaultReferenceODT mfp f)) =
176+ IO.getDefaultReferenceODT mfp >>= runIO . f
177+runIO (Free (NewStdGen f)) = IO.newStdGen >>= runIO . f
178+runIO (Free (NewUniqueHash f)) = hashUnique <$> IO.newUnique >>= runIO . f
179+runIO (Free (ReadFileStrict fp f)) = B.readFile fp >>= runIO . f
180+runIO (Free (ReadFileLazy fp f)) = BL.readFile fp >>= runIO . f
181+runIO (Free (ReadFileUTF8 fp f)) = UTF8.readFile fp >>= runIO . f
182+runIO (Free (ReadDataFile mfp fp f)) = IO.readDataFile mfp fp >>= runIO . f
183+runIO (Free (Fail s)) = M.fail s
184+runIO (Free (FetchItem sourceUrl nm f)) =
185+ IO.fetchItem sourceUrl nm >>= runIO . f
186+runIO (Free (FetchItem' media sourceUrl nm f)) =
187+ IO.fetchItem' media sourceUrl nm >>= runIO . f
188+runIO (Free (Warn s nxt)) = IO.warn s >> runIO nxt
189+runIO (Free (Glob s f)) = IO.glob s >>= runIO . f
190+runIO (Pure r) = return r
191+
192+data TestState = TestState { stStdGen :: StdGen
193+ , stWord8Store :: [Word8] -- should be
194+ -- inifinite,
195+ -- i.e. [1..]
196+ , stWarnings :: [String]
197+ , stUniqStore :: [Int] -- should be
198+ -- inifinite and
199+ -- contain every
200+ -- element at most
201+ -- once, e.g. [1..]
202+ }
203+
204+data TestEnv = TestEnv { envEnv :: [(String, String)]
205+ , envTime :: UTCTime
206+ , envReferenceDocx :: Archive
207+ , envReferenceODT :: Archive
208+ , envFiles :: [(FilePath, B.ByteString)]
209+ , envUserDataDir :: [(FilePath, B.ByteString)]
210+ , envCabalDataDir :: [(FilePath, B.ByteString)]
211+ , envFontFiles :: [FilePath]
212+ }
213+
214+data TestException = TestException
215+ deriving (Show, Typeable)
216+
217+instance E.Exception TestException
218+
219+type Testing = ReaderT TestEnv (State TestState)
220+
221+runTest :: PandocAction nxt -> Testing nxt
222+runTest (Free (LookupEnv s f)) = do
223+ env <- asks envEnv
224+ return (lookup s env) >>= runTest . f
225+runTest (Free (GetCurrentTime f)) =
226+ asks envTime >>= runTest . f
227+runTest (Free (GetPOSIXTime f)) =
228+ (utcTimeToPOSIXSeconds <$> asks envTime) >>= runTest . f
229+runTest (Free (GetDefaultReferenceDocx _ f)) =
230+ asks envReferenceDocx >>= runTest . f
231+runTest (Free (GetDefaultReferenceODT _ f)) =
232+ asks envReferenceODT >>= runTest . f
233+runTest (Free (NewStdGen f)) = do
234+ g <- gets stStdGen
235+ let (_, nxtGen) = next g
236+ modify $ \st -> st { stStdGen = nxtGen }
237+ return g >>= runTest . f
238+runTest (Free (NewUniqueHash f)) = do
239+ uniqs <- gets stUniqStore
240+ case uniqs of
241+ u : us -> do
242+ modify $ \st -> st { stUniqStore = us }
243+ return u >>= runTest . f
244+ _ -> M.fail "uniq store ran out of elements"
245+runTest (Free (ReadFileStrict fp f)) = do
246+ fps <- asks envFiles
247+ case lookup fp fps of
248+ Just bs -> return bs >>= runTest . f
249+ Nothing -> error "openFile: does not exist"
250+runTest (Free (ReadFileLazy fp f)) = do
251+ fps <- asks envFiles
252+ case lookup fp fps of
253+ Just bs -> return (BL.fromStrict bs) >>= runTest . f
254+ Nothing -> error "openFile: does not exist"
255+runTest (Free (ReadFileUTF8 fp f)) = do
256+ fps <- asks envFiles
257+ case lookup fp fps of
258+ Just bs -> return (UTF8.toString bs) >>= runTest . f
259+ Nothing -> error "openFile: does not exist"
260+-- A few different cases of readDataFile to reimplement, for when
261+-- there is no filepath and it falls through to readDefaultDataFile
262+runTest (Free (ReadDataFile Nothing "reference.docx" f)) = do
263+ (B.concat . BL.toChunks . fromArchive) <$>
264+ (runTest $ getDefaultReferenceDocx Nothing) >>=
265+ runTest . f
266+runTest (Free (ReadDataFile Nothing "reference.odt" f)) = do
267+ (B.concat . BL.toChunks . fromArchive) <$>
268+ (runTest $ getDefaultReferenceODT Nothing) >>=
269+ runTest . f
270+runTest (Free (ReadDataFile Nothing fname f)) = do
271+ let fname' = if fname == "MANUAL.txt" then fname else "data" </> fname
272+ runTest (readFileStrict fname') >>= runTest . f
273+runTest (Free (ReadDataFile (Just userDir) fname f)) = do
274+ userDirFiles <- asks envUserDataDir
275+ case lookup (userDir </> fname) userDirFiles of
276+ Just bs -> return bs >>= runTest . f
277+ Nothing -> runTest (readDataFile Nothing fname) >>= runTest . f
278+runTest (Free (Fail s)) = M.fail s
279+runTest (Free (FetchItem _ fp f)) = do
280+ fps <- asks envFiles
281+ case lookup fp fps of
282+ Just bs -> return (Right (bs, getMimeType fp)) >>= runTest . f
283+ Nothing -> return (Left $ E.toException TestException) >>= runTest . f
284+runTest (Free (FetchItem' media sourceUrl nm f)) = do
285+ case lookupMedia nm media of
286+ Nothing -> runTest (fetchItem sourceUrl nm) >>= runTest . f
287+ Just (mime, bs) -> return (Right (B.concat $ BL.toChunks bs, Just mime)) >>= runTest . f
288+runTest (Free (Warn s nxt)) = do
289+ modify $ \st -> st { stWarnings = s : stWarnings st }
290+ runTest nxt
291+runTest (Free (Glob s f)) = do
292+ fontFiles <- asks envFontFiles
293+ return (filter (match (compile s)) fontFiles) >>= runTest . f
294+runTest (Pure r) = return r
295+
296+
297+
src/Text/Pandoc/UUID.hs+12−11
@@ -29,13 +29,12 @@ UUID generation using Version 4 (random method) described
2929in RFC4122. See http://tools.ietf.org/html/rfc4122
3030-}
3131
32−module Text.Pandoc.UUID ( UUID, getRandomUUID ) where
32+module Text.Pandoc.UUID ( UUID(..), getRandomUUID, getUUID ) where
3333
3434import Text.Printf ( printf )
35−import System.Random ( randomIO )
35+import System.Random ( RandomGen, randoms, getStdGen )
3636import Data.Word
3737import Data.Bits ( setBit, clearBit )
38−import Control.Monad ( liftM )
3938
4039data UUID = UUID Word8 Word8 Word8 Word8 Word8 Word8 Word8 Word8
4140 Word8 Word8 Word8 Word8 Word8 Word8 Word8 Word8
@@ -64,14 +63,16 @@ instance Show UUID where
6463 printf "%02x" o ++
6564 printf "%02x" p
6665
67−getRandomUUID :: IO UUID
68−getRandomUUID = do
69− let getRN :: a -> IO Word8
70− getRN _ = liftM fromIntegral (randomIO :: IO Int)
71− [a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p] <- mapM getRN ([1..16] :: [Int])
66+getUUID :: RandomGen g => g -> UUID
67+getUUID gen =
68+ let [a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p] = take 16 $ randoms gen :: [Word8]
7269 -- set variant
73− let i' = i `setBit` 7 `clearBit` 6
70+ i' = i `setBit` 7 `clearBit` 6
7471 -- set version (0100 for random)
75− let g' = g `clearBit` 7 `setBit` 6 `clearBit` 5 `clearBit` 4
76− return $ UUID a b c d e f g' h i' j k l m n o p
72+ g' = g `clearBit` 7 `setBit` 6 `clearBit` 5 `clearBit` 4
73+ in
74+ UUID a b c d e f g' h i' j k l m n o p
75+
76+getRandomUUID :: IO UUID
77+getRandomUUID = getUUID <$> getStdGen
7778
src/Text/Pandoc/Writers/Docx.hs+37−28
@@ -1,4 +1,4 @@
1−{-# LANGUAGE ScopedTypeVariables, PatternGuards, ViewPatterns #-}
1+{-# LANGUAGE ScopedTypeVariables, PatternGuards, ViewPatterns, DeriveFunctor #-}
22{-
33Copyright (C) 2012-2015 John MacFarlane <jgm@berkeley.edu>
44
@@ -28,7 +28,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2828
2929Conversion of 'Pandoc' documents to docx.
3030-}
31−module Text.Pandoc.Writers.Docx ( writeDocx ) where
31+module Text.Pandoc.Writers.Docx ( writeDocx, writeDocxPure ) where
3232import Data.List ( intercalate, isPrefixOf, isSuffixOf )
3333import qualified Data.ByteString as B
3434import qualified Data.ByteString.Lazy as BL
@@ -38,7 +38,6 @@ import qualified Data.Set as Set
3838import qualified Text.Pandoc.UTF8 as UTF8
3939import Codec.Archive.Zip
4040import Data.Time.Clock.POSIX
41−import System.Environment
4241import Text.Pandoc.Compat.Time
4342import Text.Pandoc.Definition
4443import Text.Pandoc.Generic
@@ -57,8 +56,7 @@ import Text.Pandoc.Readers.Docx.Util (elemName)
5756import Control.Monad.Reader
5857import Control.Monad.State
5958import Text.Highlighting.Kate
60−import Data.Unique (hashUnique, newUnique)
61−import System.Random (randomRIO)
59+import System.Random (randomR)
6260import Text.Printf (printf)
6361import qualified Control.Exception as E
6462import Data.Monoid ((<>))
@@ -67,6 +65,8 @@ import Text.Pandoc.MIME (MimeType, getMimeType, getMimeTypeDef,
6765import Control.Applicative ((<|>))
6866import Data.Maybe (fromMaybe, mapMaybe, maybeToList, isNothing)
6967import Data.Char (ord, isSpace, toLower)
68+import Text.Pandoc.Free (PandocAction, runIO)
69+import qualified Text.Pandoc.Free as P
7070
7171data ListMarker = NoMarker
7272 | BulletMarker
@@ -146,7 +146,7 @@ defaultWriterState = WriterState{
146146 , stDynamicTextProps = []
147147 }
148148
149−type WS = ReaderT WriterEnv (StateT WriterState IO)
149+type WS = ReaderT WriterEnv (StateT WriterState (PandocAction))
150150
151151mknode :: Node t => String -> [(String,String)] -> t -> Element
152152mknode s attrs =
@@ -213,19 +213,27 @@ metaValueToInlines (MetaBlocks bs) = query return bs
213213metaValueToInlines (MetaBool b) = [Str $ show b]
214214metaValueToInlines _ = []
215215
216−-- | Produce an Docx file from a Pandoc document.
216+
217+
217218writeDocx :: WriterOptions -- ^ Writer options
218219 -> Pandoc -- ^ Document to convert
219220 -> IO BL.ByteString
220−writeDocx opts doc@(Pandoc meta _) = do
221+writeDocx opts doc = runIO $ writeDocxPure opts doc
222+
223+
224+-- | Produce an Docx file from a Pandoc document.
225+writeDocxPure :: WriterOptions -- ^ Writer options
226+ -> Pandoc -- ^ Document to convert
227+ -> PandocAction BL.ByteString
228+writeDocxPure opts doc@(Pandoc meta _) = do
221229 let datadir = writerUserDataDir opts
222230 let doc' = walk fixDisplayMath $ doc
223− username <- lookup "USERNAME" <$> getEnvironment
224− utctime <- getCurrentTime
225− distArchive <- getDefaultReferenceDocx datadir
231+ username <- P.lookupEnv "USERNAME"
232+ utctime <- P.getCurrentTime
233+ distArchive <- P.getDefaultReferenceDocx datadir
226234 refArchive <- case writerReferenceDocx opts of
227− Just f -> liftM (toArchive . toLazy) $ B.readFile f
228− Nothing -> getDefaultReferenceDocx datadir
235+ Just f -> liftM (toArchive . toLazy) $ P.readFileStrict f
236+ Nothing -> P.getDefaultReferenceDocx datadir
229237
230238 parsedDoc <- parseXml refArchive distArchive "word/document.xml"
231239 let wname f qn = qPrefix qn == Just "w" && f (qName qn)
@@ -603,7 +611,7 @@ styleToOpenXml sm style =
603611 $ backgroundColor style )
604612 ]
605613
606−copyChildren :: Archive -> Archive -> String -> Integer -> [String] -> IO Entry
614+copyChildren :: Archive -> Archive -> String -> Integer -> [String] -> PandocAction Entry
607615copyChildren refArchive distArchive path timestamp elNames = do
608616 ref <- parseXml refArchive distArchive path
609617 dist <- parseXml distArchive distArchive path
@@ -622,7 +630,7 @@ copyChildren refArchive distArchive path timestamp elNames = do
622630baseListId :: Int
623631baseListId = 1000
624632
625−mkNumbering :: [ListMarker] -> IO [Element]
633+mkNumbering :: [ListMarker] -> PandocAction [Element]
626634mkNumbering lists = do
627635 elts <- mapM mkAbstractNum (ordNub lists)
628636 return $ elts ++ zipWith mkNum lists [baseListId..(baseListId + length lists - 1)]
@@ -638,9 +646,10 @@ mkNum marker numid =
638646 map (\lvl -> mknode "w:lvlOverride" [("w:ilvl",show (lvl :: Int))]
639647 $ mknode "w:startOverride" [("w:val",show start)] ()) [0..6]
640648
641−mkAbstractNum :: ListMarker -> IO Element
649+mkAbstractNum :: ListMarker -> PandocAction Element
642650mkAbstractNum marker = do
643− nsid <- randomRIO (0x10000000 :: Integer, 0xFFFFFFFF :: Integer)
651+ gen <- P.newStdGen
652+ let (nsid, _) = randomR (0x10000000 :: Integer, 0xFFFFFFFF :: Integer) gen
644653 return $ mknode "w:abstractNum" [("w:abstractNumId",listMarkerToId marker)]
645654 $ mknode "w:nsid" [("w:val", printf "%8x" nsid)] ()
646655 : mknode "w:multiLevelType" [("w:val","multilevel")] ()
@@ -695,6 +704,7 @@ mkLvl marker lvl =
695704getNumId :: WS Int
696705getNumId = (((baseListId - 1) +) . length) `fmap` gets stLists
697706
707+
698708makeTOC :: WriterOptions -> WS [Element]
699709makeTOC opts | writerTableOfContents opts = do
700710 let depth = "1-"++(show (writerTOCDepth opts))
@@ -781,10 +791,10 @@ rStyleM styleName = do
781791 let sty' = getStyleId styleName $ sCharStyleMap styleMaps
782792 return $ mknode "w:rStyle" [("w:val",sty')] ()
783793
784−getUniqueId :: MonadIO m => m String
794+getUniqueId :: PandocAction String
785795-- the + 20 is to ensure that there are no clashes with the rIds
786796-- already in word/document.xml.rel
787−getUniqueId = liftIO $ (show . (+ 20) . hashUnique) `fmap` newUnique
797+getUniqueId = (show . (+ 20)) <$> P.newUniqueHash
788798
789799-- | Key for specifying user-defined docx styles.
790800dynamicStyleKey :: String
@@ -825,7 +835,7 @@ blockToOpenXML' opts (Header lev (ident,_,_) lst) = do
825835 then uniqueIdent lst usedIdents
826836 else ident
827837 modify $ \s -> s{ stSectionIds = Set.insert bookmarkName $ stSectionIds s }
828− id' <- getUniqueId
838+ id' <- (lift . lift) getUniqueId
829839 let bookmarkStart = mknode "w:bookmarkStart" [("w:id", id')
830840 ,("w:name",bookmarkName)] ()
831841 let bookmarkEnd = mknode "w:bookmarkEnd" [("w:id", id')] ()
@@ -1133,7 +1143,7 @@ inlineToOpenXML' opts (Code attrs str) = do
11331143 else unhighlighted
11341144inlineToOpenXML' opts (Note bs) = do
11351145 notes <- gets stFootnotes
1136− notenum <- getUniqueId
1146+ notenum <- (lift . lift) getUniqueId
11371147 footnoteStyle <- rStyleM "Footnote Reference"
11381148 let notemarker = mknode "w:r" []
11391149 [ mknode "w:rPr" [] footnoteStyle
@@ -1164,7 +1174,7 @@ inlineToOpenXML' opts (Link _ txt (src,_)) = do
11641174 id' <- case M.lookup src extlinks of
11651175 Just i -> return i
11661176 Nothing -> do
1167− i <- ("rId"++) `fmap` getUniqueId
1177+ i <- ("rId"++) `fmap` ((lift . lift) getUniqueId)
11681178 modify $ \st -> st{ stExternalLinks =
11691179 M.insert src i extlinks }
11701180 return i
@@ -1176,15 +1186,14 @@ inlineToOpenXML' opts (Image attr alt (src, title)) = do
11761186 case M.lookup src imgs of
11771187 Just (_,_,_,elt,_) -> return [elt]
11781188 Nothing -> do
1179− res <- liftIO $
1180− fetchItem' (writerMediaBag opts) (writerSourceURL opts) src
1189+ res <- (lift . lift) $ P.fetchItem' (writerMediaBag opts) (writerSourceURL opts) src
11811190 case res of
11821191 Left (_ :: E.SomeException) -> do
1183− liftIO $ warn $ "Could not find image `" ++ src ++ "', skipping..."
1192+ (lift . lift) $ P.warn ("Could not find image `" ++ src ++ "', skipping...")
11841193 -- emit alt text
11851194 inlinesToOpenXML opts alt
11861195 Right (img, mt) -> do
1187− ident <- ("rId"++) `fmap` getUniqueId
1196+ ident <- ("rId"++) `fmap` ((lift . lift) getUniqueId)
11881197 let (xpt,ypt) = desiredSizeInPoints opts attr
11891198 (either (const def) id (imageSize img))
11901199 -- 12700 emu = 1 pt
@@ -1262,13 +1271,13 @@ defaultFootnotes = [ mknode "w:footnote"
12621271 [ mknode "w:r" [] $
12631272 [ mknode "w:continuationSeparator" [] ()]]]]
12641273
1265−parseXml :: Archive -> Archive -> String -> IO Element
1274+parseXml :: Archive -> Archive -> String -> PandocAction Element
12661275parseXml refArchive distArchive relpath =
12671276 case findEntryByPath relpath refArchive `mplus`
12681277 findEntryByPath relpath distArchive of
12691278 Nothing -> fail $ relpath ++ " missing in reference docx"
12701279 Just e -> case parseXMLDoc . UTF8.toStringLazy . fromEntry $ e of
1271− Nothing -> fail $ relpath ++ " corrupt in reference docx"
1280+ Nothing -> P.fail $ relpath ++ " corrupt in reference docx"
12721281 Just d -> return d
12731282
12741283-- | Scales the image to fit the page
src/Text/Pandoc/Writers/EPUB.hs+78−61
@@ -28,26 +28,22 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2828
2929Conversion of 'Pandoc' documents to EPUB.
3030-}
31−module Text.Pandoc.Writers.EPUB ( writeEPUB ) where
32−import Data.IORef ( IORef, newIORef, readIORef, modifyIORef )
31+module Text.Pandoc.Writers.EPUB ( writeEPUB, writeEPUBPure ) where
3332import qualified Data.Map as M
3433import qualified Data.Set as Set
3534import Data.Maybe ( fromMaybe, catMaybes )
3635import Data.List ( isPrefixOf, isInfixOf, intercalate )
37−import System.Environment ( getEnv )
3836import Text.Printf (printf)
3937import System.FilePath ( takeExtension, takeFileName )
40−import System.FilePath.Glob ( namesMatching )
4138import Network.HTTP ( urlEncode )
4239import qualified Data.ByteString.Lazy as B
4340import qualified Data.ByteString.Lazy.Char8 as B8
4441import qualified Text.Pandoc.UTF8 as UTF8
4542import Codec.Archive.Zip ( emptyArchive, addEntryToArchive, eRelativePath, fromEntry , Entry, toEntry, fromArchive)
46−import Data.Time.Clock.POSIX ( getPOSIXTime )
4743import Text.Pandoc.Compat.Time
4844import Text.Pandoc.Shared ( renderTags', safeRead, uniqueIdent, trim
49− , normalizeDate, readDataFile, stringify, warn
50− , hierarchicalize, fetchItem' )
45+ , normalizeDate, stringify
46+ , hierarchicalize )
5147import qualified Text.Pandoc.Shared as S (Element(..))
5248import Text.Pandoc.Builder (fromList, setMeta)
5349import Text.Pandoc.Options ( WriterOptions(..)
@@ -57,18 +53,19 @@ import Text.Pandoc.Options ( WriterOptions(..)
5753 , ObfuscationMethod(NoObfuscation) )
5854import Text.Pandoc.Definition
5955import Text.Pandoc.Walk (walk, walkM, query)
60−import Control.Monad.State (modify, get, State, put, evalState)
61−import Control.Monad (mplus, liftM, when)
56+import Text.Pandoc.UUID (getUUID)
57+import Control.Monad.State (modify, get, gets, State, StateT, put, evalState, evalStateT, lift)
58+import Control.Monad (mplus, when)
6259import Text.XML.Light ( unode, Element(..), unqual, Attr(..), add_attrs
6360 , strContent, lookupAttr, Node(..), QName(..), parseXML
6461 , onlyElems, node, ppElement)
65−import Text.Pandoc.UUID (getRandomUUID)
6662import Text.Pandoc.Writers.HTML ( writeHtml )
6763import Data.Char ( toLower, isDigit, isAlphaNum )
6864import Text.Pandoc.MIME (MimeType, getMimeType, extensionFromMimeType)
69−import qualified Control.Exception as E
7065import Text.Blaze.Html.Renderer.Utf8 (renderHtml)
7166import Text.HTML.TagSoup (Tag(TagOpen), fromAttrib, parseTags)
67+import Text.Pandoc.Free (PandocAction, runIO)
68+import qualified Text.Pandoc.Free as P
7269
7370-- A Chapter includes a list of blocks and maybe a section
7471-- number offset. Note, some chapters are unnumbered. The section
@@ -76,6 +73,11 @@ import Text.HTML.TagSoup (Tag(TagOpen), fromAttrib, parseTags)
7673-- in filenames, chapter0003.xhtml.
7774data Chapter = Chapter (Maybe [Int]) [Block]
7875
76+data EPUBState = EPUBState { stMediaPaths :: [(FilePath, (FilePath, Maybe Entry))]
77+ }
78+
79+type E = StateT EPUBState PandocAction
80+
7981data EPUBMetadata = EPUBMetadata{
8082 epubIdentifier :: [Identifier]
8183 , epubTitle :: [Title]
@@ -143,7 +145,7 @@ removeNote :: Inline -> Inline
143145removeNote (Note _) = Str ""
144146removeNote x = x
145147
146−getEPUBMetadata :: WriterOptions -> Meta -> IO EPUBMetadata
148+getEPUBMetadata :: WriterOptions -> Meta -> E EPUBMetadata
147149getEPUBMetadata opts meta = do
148150 let md = metadataFromMeta opts meta
149151 let elts = onlyElems $ parseXML $ writerEpubMetadata opts
@@ -151,7 +153,7 @@ getEPUBMetadata opts meta = do
151153 let addIdentifier m =
152154 if null (epubIdentifier m)
153155 then do
154− randomId <- fmap show getRandomUUID
156+ randomId <- (show . getUUID) <$> lift P.newStdGen
155157 return $ m{ epubIdentifier = [Identifier randomId Nothing] }
156158 else return m
157159 let addLanguage m =
@@ -159,16 +161,19 @@ getEPUBMetadata opts meta = do
159161 then case lookup "lang" (writerVariables opts) of
160162 Just x -> return m{ epubLanguage = x }
161163 Nothing -> do
162− localeLang <- E.catch (liftM
163− (map (\c -> if c == '_' then '-' else c) .
164− takeWhile (/='.')) $ getEnv "LANG")
165− (\e -> let _ = (e :: E.SomeException) in return "en-US")
164+ mLang <- lift $ P.lookupEnv "LANG"
165+ let localeLang =
166+ case mLang of
167+ Just lang ->
168+ map (\c -> if c == '_' then '-' else c) $
169+ takeWhile (/='.') lang
170+ Nothing -> "en-US"
166171 return m{ epubLanguage = localeLang }
167172 else return m
168173 let fixDate m =
169174 if null (epubDate m)
170175 then do
171− currentTime <- getCurrentTime
176+ currentTime <- lift P.getCurrentTime
172177 return $ m{ epubDate = [ Date{
173178 dateText = showDateTimeISO8601 currentTime
174179 , dateEvent = Nothing } ] }
@@ -333,10 +338,24 @@ metadataFromMeta opts meta = EPUBMetadata{
333338writeEPUB :: WriterOptions -- ^ Writer options
334339 -> Pandoc -- ^ Document to convert
335340 -> IO B.ByteString
336−writeEPUB opts doc@(Pandoc meta _) = do
341+writeEPUB opts doc = runIO $ writeEPUBPure opts doc
342+
343+writeEPUBPure :: WriterOptions -- ^ Writer options
344+ -> Pandoc -- ^ Document to convert
345+ -> PandocAction B.ByteString
346+writeEPUBPure opts doc =
347+ let initState = EPUBState { stMediaPaths = []
348+ }
349+ in
350+ evalStateT (pandocToEPUB opts doc) initState
351+
352+pandocToEPUB :: WriterOptions
353+ -> Pandoc
354+ -> E B.ByteString
355+pandocToEPUB opts doc@(Pandoc meta _) = do
337356 let version = fromMaybe EPUB2 (writerEpubVersion opts)
338357 let epub3 = version == EPUB3
339− epochtime <- floor `fmap` getPOSIXTime
358+ epochtime <- floor <$> lift P.getPOSIXTime
340359 let mkEntry path content = toEntry path epochtime content
341360 let vars = ("epub3", if epub3 then "true" else "false")
342361 : ("css", "stylesheet.css")
@@ -362,7 +381,7 @@ writeEPUB opts doc@(Pandoc meta _) = do
362381 let cpContent = renderHtml $ writeHtml
363382 opts'{ writerVariables = ("coverpage","true"):vars }
364383 (Pandoc meta [RawBlock (Format "html") $ "<div id=\"cover-image\">\n<img src=\"" ++ coverImage ++ "\" alt=\"cover image\" />\n</div>"])
365− imgContent <- B.readFile img
384+ imgContent <- lift $ P.readFileLazy img
366385 return ( [mkEntry "cover.xhtml" cpContent]
367386 , [mkEntry coverImage imgContent] )
368387
@@ -373,18 +392,17 @@ writeEPUB opts doc@(Pandoc meta _) = do
373392 let tpEntry = mkEntry "title_page.xhtml" tpContent
374393
375394 -- handle pictures
376− mediaRef <- newIORef []
377− Pandoc _ blocks <- walkM (transformInline opts' mediaRef) doc >>=
378− walkM (transformBlock opts' mediaRef)
379− picEntries <- (catMaybes . map (snd . snd)) <$> readIORef mediaRef
380−
395+ -- mediaRef <- P.newIORef []
396+ Pandoc _ blocks <- walkM (transformInline opts') doc >>=
397+ walkM (transformBlock opts')
398+ picEntries <- (catMaybes . map (snd . snd)) <$> (gets stMediaPaths)
381399 -- handle fonts
382400 let matchingGlob f = do
383− xs <- namesMatching f
401+ xs <- lift $ P.glob f
384402 when (null xs) $
385− warn $ f ++ " did not match any font files."
403+ lift $ P.warn $ f ++ " did not match any font files."
386404 return xs
387− let mkFontEntry f = mkEntry (takeFileName f) `fmap` B.readFile f
405+ let mkFontEntry f = mkEntry (takeFileName f) `fmap` (lift $ P.readFileLazy f)
388406 fontFiles <- concat <$> mapM matchingGlob (writerEpubFonts opts')
389407 fontEntries <- mapM mkFontEntry fontFiles
390408
@@ -521,7 +539,7 @@ writeEPUB opts doc@(Pandoc meta _) = do
521539 let uuid = case epubIdentifier metadata of
522540 (x:_) -> identifierText x -- use first identifier as UUID
523541 [] -> error "epubIdentifier is null" -- shouldn't happen
524− currentTime <- getCurrentTime
542+ currentTime <- lift $ P.getCurrentTime
525543 let contentsData = UTF8.fromStringLazy $ ppTopElement $
526544 unode "package" ! [("version", case version of
527545 EPUB2 -> "2.0"
@@ -693,10 +711,10 @@ writeEPUB opts doc@(Pandoc meta _) = do
693711
694712 -- stylesheet
695713 stylesheet <- case epubStylesheet metadata of
696− Just (StylesheetPath fp) -> UTF8.readFile fp
714+ Just (StylesheetPath fp) -> lift $ P.readFileUTF8 fp
697715 Just (StylesheetContents s) -> return s
698716 Nothing -> UTF8.toString `fmap`
699− readDataFile (writerUserDataDir opts) "epub.css"
717+ (lift $ P.readDataFile (writerUserDataDir opts) "epub.css")
700718 let stylesheetEntry = mkEntry "stylesheet.css" $ UTF8.fromStringLazy stylesheet
701719
702720 -- construct archive
@@ -813,78 +831,77 @@ showDateTimeISO8601 :: UTCTime -> String
813831showDateTimeISO8601 = formatTime defaultTimeLocale "%FT%TZ"
814832
815833transformTag :: WriterOptions
816− -> IORef [(FilePath, (FilePath, Maybe Entry))] -- ^ (oldpath, newpath, entry) media
834+ -- -> IORef [(FilePath, (FilePath, Maybe Entry))] -- ^ (oldpath, newpath, entry) media
817835 -> Tag String
818− -> IO (Tag String)
819−transformTag opts mediaRef tag@(TagOpen name attr)
836+ -> E (Tag String)
837+transformTag opts tag@(TagOpen name attr)
820838 | name `elem` ["video", "source", "img", "audio"] &&
821839 lookup "data-external" attr == Nothing = do
822840 let src = fromAttrib "src" tag
823841 let poster = fromAttrib "poster" tag
824− newsrc <- modifyMediaRef opts mediaRef src
825− newposter <- modifyMediaRef opts mediaRef poster
842+ newsrc <- modifyMediaRef opts src
843+ newposter <- modifyMediaRef opts poster
826844 let attr' = filter (\(x,_) -> x /= "src" && x /= "poster") attr ++
827845 [("src", newsrc) | not (null newsrc)] ++
828846 [("poster", newposter) | not (null newposter)]
829847 return $ TagOpen name attr'
830−transformTag _ _ tag = return tag
848+transformTag _ tag = return tag
831849
832850modifyMediaRef :: WriterOptions
833− -> IORef [(FilePath, (FilePath, Maybe Entry))]
834851 -> FilePath
835− -> IO FilePath
836−modifyMediaRef _ _ "" = return ""
837−modifyMediaRef opts mediaRef oldsrc = do
838− media <- readIORef mediaRef
852+ -> E FilePath
853+modifyMediaRef _ "" = return ""
854+modifyMediaRef opts oldsrc = do
855+ media <- gets stMediaPaths
839856 case lookup oldsrc media of
840857 Just (n,_) -> return n
841858 Nothing -> do
842− res <- fetchItem' (writerMediaBag opts)
859+ res <- lift $ P.fetchItem' (writerMediaBag opts)
843860 (writerSourceURL opts) oldsrc
844861 (new, mbEntry) <-
845862 case res of
846863 Left _ -> do
847− warn $ "Could not find media `" ++ oldsrc ++ "', skipping..."
864+ lift $ P.warn $ "Could not find media `" ++ oldsrc ++ "', skipping..."
848865 return (oldsrc, Nothing)
849866 Right (img,mbMime) -> do
850867 let new = "media/file" ++ show (length media) ++
851868 fromMaybe (takeExtension (takeWhile (/='?') oldsrc))
852869 (('.':) <$> (mbMime >>= extensionFromMimeType))
853− epochtime <- floor `fmap` getPOSIXTime
870+ epochtime <- floor `fmap` lift P.getPOSIXTime
854871 let entry = toEntry new epochtime $ B.fromChunks . (:[]) $ img
855872 return (new, Just entry)
856− modifyIORef mediaRef ( (oldsrc, (new, mbEntry)): )
873+ modify $ \st -> st{ stMediaPaths = (oldsrc, (new, mbEntry)):media}
857874 return new
858875
859876transformBlock :: WriterOptions
860− -> IORef [(FilePath, (FilePath, Maybe Entry))] -- ^ (oldpath, newpath, entry) media
877+ -- -> IORef [(FilePath, (FilePath, Maybe Entry))] -- ^ (oldpath, newpath, entry) media
861878 -> Block
862− -> IO Block
863−transformBlock opts mediaRef (RawBlock fmt raw)
879+ -> E Block
880+transformBlock opts (RawBlock fmt raw)
864881 | fmt == Format "html" = do
865882 let tags = parseTags raw
866− tags' <- mapM (transformTag opts mediaRef) tags
883+ tags' <- mapM (transformTag opts) tags
867884 return $ RawBlock fmt (renderTags' tags')
868−transformBlock _ _ b = return b
885+transformBlock _ b = return b
869886
870887transformInline :: WriterOptions
871− -> IORef [(FilePath, (FilePath, Maybe Entry))] -- ^ (oldpath, newpath) media
888+ -- -> IORef [(FilePath, (FilePath, Maybe Entry))] -- ^ (oldpath, newpath) media
872889 -> Inline
873− -> IO Inline
874−transformInline opts mediaRef (Image attr lab (src,tit)) = do
875− newsrc <- modifyMediaRef opts mediaRef src
890+ -> E Inline
891+transformInline opts (Image attr lab (src,tit)) = do
892+ newsrc <- modifyMediaRef opts src
876893 return $ Image attr lab (newsrc, tit)
877−transformInline opts mediaRef (x@(Math t m))
894+transformInline opts (x@(Math t m))
878895 | WebTeX url <- writerHTMLMathMethod opts = do
879− newsrc <- modifyMediaRef opts mediaRef (url ++ urlEncode m)
896+ newsrc <- modifyMediaRef opts (url ++ urlEncode m)
880897 let mathclass = if t == DisplayMath then "display" else "inline"
881898 return $ Span ("",["math",mathclass],[]) [Image nullAttr [x] (newsrc, "")]
882−transformInline opts mediaRef (RawInline fmt raw)
899+transformInline opts (RawInline fmt raw)
883900 | fmt == Format "html" = do
884901 let tags = parseTags raw
885− tags' <- mapM (transformTag opts mediaRef) tags
902+ tags' <- mapM (transformTag opts) tags
886903 return $ RawInline fmt (renderTags' tags')
887−transformInline _ _ x = return x
904+transformInline _ x = return x
888905
889906(!) :: (t -> Element) -> [(String, String)] -> t -> Element
890907(!) f attrs n = add_attrs (map (\(k,v) -> Attr (unqual k) v) attrs) (f n)
src/Text/Pandoc/Writers/FB2.hs+82−95
@@ -25,29 +25,28 @@ FictionBook is an XML-based e-book format. For more information see:
2525<http://www.fictionbook.org/index.php/Eng:XML_Schema_Fictionbook_2.1>
2626
2727-}
28−module Text.Pandoc.Writers.FB2 (writeFB2) where
28+module Text.Pandoc.Writers.FB2 (writeFB2, writeFB2Pure) where
2929
30−import Control.Monad.State (StateT, evalStateT, get, modify)
31−import Control.Monad.State (liftM, liftM2, liftIO)
30+import Control.Monad.State (StateT, evalStateT, get, modify, lift)
31+import Control.Monad.State (liftM)
3232import Data.ByteString.Base64 (encode)
3333import Data.Char (toLower, isSpace, isAscii, isControl)
3434import Data.List (intersperse, intercalate, isPrefixOf, stripPrefix)
3535import Data.Either (lefts, rights)
36−import Network.Browser (browse, request, setAllowRedirects, setOutHandler)
37−import Network.HTTP (catchIO_, getRequest, getHeaders, getResponseBody)
38−import Network.HTTP (lookupHeader, HeaderName(..), urlEncode)
39−import Network.URI (isURI, unEscapeString)
40−import System.FilePath (takeExtension)
36+import Network.HTTP (urlEncode)
37+import Network.URI (isURI)
4138import Text.XML.Light
42−import qualified Control.Exception as E
43−import qualified Data.ByteString as B
4439import qualified Text.XML.Light as X
4540import qualified Text.XML.Light.Cursor as XC
41+import qualified Data.ByteString.Char8 as B8
42+
4643
4744import Text.Pandoc.Definition
4845import Text.Pandoc.Options (WriterOptions(..), HTMLMathMethod(..), def)
4946import Text.Pandoc.Shared (orderedListMarkers, isHeaderBlock, capitalize,
5047 linesToPara)
48+import Text.Pandoc.Free (PandocAction, runIO)
49+import qualified Text.Pandoc.Free as P
5150
5251-- | Data to be written at the end of the document:
5352-- (foot)notes, URLs, references, images.
@@ -60,7 +59,7 @@ data FbRenderState = FbRenderState
6059 } deriving (Show)
6160
6261-- | FictionBook building monad.
63−type FBM = StateT FbRenderState IO
62+type FBM = StateT FbRenderState PandocAction
6463
6564newFB :: FbRenderState
6665newFB = FbRenderState { footnotes = [], imagesToFetch = []
@@ -76,14 +75,24 @@ instance Show ImageMode where
7675writeFB2 :: WriterOptions -- ^ conversion options
7776 -> Pandoc -- ^ document to convert
7877 -> IO String -- ^ FictionBook2 document (not encoded yet)
79−writeFB2 opts (Pandoc meta blocks) = flip evalStateT newFB $ do
78+writeFB2 opts doc = runIO $ writeFB2Pure opts doc
79+
80+writeFB2Pure :: WriterOptions
81+ -> Pandoc
82+ -> PandocAction String
83+writeFB2Pure opts doc = flip evalStateT newFB $ pandocToFB2 opts doc
84+
85+pandocToFB2 :: WriterOptions
86+ -> Pandoc
87+ -> FBM String
88+pandocToFB2 opts (Pandoc meta blocks) = do
8089 modify (\s -> s { writerOptions = opts { writerStandalone = True } })
8190 desc <- description meta
8291 fp <- frontpage meta
8392 secs <- renderSections 1 blocks
8493 let body = el "body" $ fp ++ secs
8594 notes <- renderFootnotes
86− (imgs,missing) <- liftM imagesToFetch get >>= \s -> liftIO (fetchImages s)
95+ (imgs,missing) <- liftM imagesToFetch get >>= \s -> lift (fetchImages s)
8796 let body' = replaceImagesWithAlt missing body
8897 let fb2_xml = el "FictionBook" (fb2_attrs, [desc, body'] ++ notes ++ imgs)
8998 return $ xml_head ++ (showContent fb2_xml) ++ "\n"
@@ -94,53 +103,58 @@ writeFB2 opts (Pandoc meta blocks) = flip evalStateT newFB $ do
94103 xlink = "http://www.w3.org/1999/xlink"
95104 in [ uattr "xmlns" xmlns
96105 , attr ("xmlns", "l") xlink ]
97− --
98− frontpage :: Meta -> FBM [Content]
99− frontpage meta' = do
100− t <- cMapM toXml . docTitle $ meta'
101− return $
102− [ el "title" (el "p" t)
103− , el "annotation" (map (el "p" . cMap plain)
104− (docAuthors meta' ++ [docDate meta']))
105− ]
106− description :: Meta -> FBM Content
107− description meta' = do
108− bt <- booktitle meta'
109− let as = authors meta'
110− dd <- docdate meta'
111− return $ el "description"
112− [ el "title-info" (bt ++ as ++ dd)
113− , el "document-info" [ el "program-used" "pandoc" ] -- FIXME: +version
114− ]
115− booktitle :: Meta -> FBM [Content]
116− booktitle meta' = do
117− t <- cMapM toXml . docTitle $ meta'
118− return $ if null t
119− then []
120− else [ el "book-title" t ]
121− authors :: Meta -> [Content]
122− authors meta' = cMap author (docAuthors meta')
123− author :: [Inline] -> [Content]
124− author ss =
125− let ws = words . cMap plain $ ss
126− email = (el "email") `fmap` (take 1 $ filter ('@' `elem`) ws)
127− ws' = filter ('@' `notElem`) ws
128− names = case ws' of
129− (nickname:[]) -> [ el "nickname" nickname ]
130− (fname:lname:[]) -> [ el "first-name" fname
131− , el "last-name" lname ]
132− (fname:rest) -> [ el "first-name" fname
133− , el "middle-name" (concat . init $ rest)
134− , el "last-name" (last rest) ]
135− ([]) -> []
136− in list $ el "author" (names ++ email)
137− docdate :: Meta -> FBM [Content]
138− docdate meta' = do
139− let ss = docDate meta'
140− d <- cMapM toXml ss
141− return $ if null d
142− then []
143− else [el "date" d]
106+
107+frontpage :: Meta -> FBM [Content]
108+frontpage meta' = do
109+ t <- cMapM toXml . docTitle $ meta'
110+ return $
111+ [ el "title" (el "p" t)
112+ , el "annotation" (map (el "p" . cMap plain)
113+ (docAuthors meta' ++ [docDate meta']))
114+ ]
115+
116+description :: Meta -> FBM Content
117+description meta' = do
118+ bt <- booktitle meta'
119+ let as = authors meta'
120+ dd <- docdate meta'
121+ return $ el "description"
122+ [ el "title-info" (bt ++ as ++ dd)
123+ , el "document-info" [ el "program-used" "pandoc" ] -- FIXME: +version
124+ ]
125+
126+booktitle :: Meta -> FBM [Content]
127+booktitle meta' = do
128+ t <- cMapM toXml . docTitle $ meta'
129+ return $ if null t
130+ then []
131+ else [ el "book-title" t ]
132+
133+authors :: Meta -> [Content]
134+authors meta' = cMap author (docAuthors meta')
135+
136+author :: [Inline] -> [Content]
137+author ss =
138+ let ws = words . cMap plain $ ss
139+ email = (el "email") `fmap` (take 1 $ filter ('@' `elem`) ws)
140+ ws' = filter ('@' `notElem`) ws
141+ names = case ws' of
142+ (nickname:[]) -> [ el "nickname" nickname ]
143+ (fname:lname:[]) -> [ el "first-name" fname
144+ , el "last-name" lname ]
145+ (fname:rest) -> [ el "first-name" fname
146+ , el "middle-name" (concat . init $ rest)
147+ , el "last-name" (last rest) ]
148+ ([]) -> []
149+ in list $ el "author" (names ++ email)
150+
151+docdate :: Meta -> FBM [Content]
152+docdate meta' = do
153+ let ss = docDate meta'
154+ d <- cMapM toXml ss
155+ return $ if null d
156+ then []
157+ else [el "date" d]
144158
145159-- | Divide the stream of blocks into sections and convert to XML
146160-- representation.
@@ -210,14 +224,14 @@ renderFootnotes = do
210224
211225-- | Fetch images and encode them for the FictionBook XML.
212226-- Return image data and a list of hrefs of the missing images.
213−fetchImages :: [(String,String)] -> IO ([Content],[String])
227+fetchImages :: [(String,String)] -> PandocAction ([Content],[String])
214228fetchImages links = do
215229 imgs <- mapM (uncurry fetchImage) links
216230 return $ (rights imgs, lefts imgs)
217231
218232-- | Fetch image data from disk or from network and make a <binary> XML section.
219233-- Return either (Left hrefOfMissingImage) or (Right xmlContent).
220−fetchImage :: String -> String -> IO (Either String Content)
234+fetchImage :: String -> String -> PandocAction (Either String Content)
221235fetchImage href link = do
222236 mbimg <-
223237 case (isURI link, readDataURI link) of
@@ -227,16 +241,11 @@ fetchImage href link = do
227241 then return (Just (mime',base64))
228242 else return Nothing
229243 (True, Just _) -> return Nothing -- not base64-encoded
230− (True, Nothing) -> fetchURL link
231− (False, _) -> do
232− d <- nothingOnError $ B.readFile (unEscapeString link)
233− let t = case map toLower (takeExtension link) of
234− ".png" -> Just "image/png"
235− ".jpg" -> Just "image/jpeg"
236− ".jpeg" -> Just "image/jpeg"
237− ".jpe" -> Just "image/jpeg"
238− _ -> Nothing -- only PNG and JPEG are supported in FB2
239− return $ liftM2 (,) t (liftM (toStr . encode) d)
244+ _ -> do
245+ response <- P.fetchItem Nothing link
246+ case response of
247+ Right (bs, Just mime) -> return $ Just (mime, B8.unpack $ encode bs)
248+ _ -> return $ Nothing
240249 case mbimg of
241250 Just (imgtype, imgdata) -> do
242251 return . Right $ el "binary"
@@ -244,11 +253,7 @@ fetchImage href link = do
244253 , uattr "content-type" imgtype]
245254 , txt imgdata )
246255 _ -> return (Left ('#':href))
247− where
248− nothingOnError :: (IO B.ByteString) -> (IO (Maybe B.ByteString))
249− nothingOnError action = liftM Just action `E.catch` omnihandler
250− omnihandler :: E.SomeException -> IO (Maybe B.ByteString)
251− omnihandler _ = return Nothing
256+
252257
253258-- | Extract mime type and encoded data from the Data URI.
254259readDataURI :: String -- ^ URI
@@ -286,24 +291,6 @@ isMimeType s =
286291 valid c = isAscii c && not (isControl c) && not (isSpace c) &&
287292 c `notElem` "()<>@,;:\\\"/[]?="
288293
289−-- | Fetch URL, return its Content-Type and binary data on success.
290−fetchURL :: String -> IO (Maybe (String, String))
291−fetchURL url = do
292− flip catchIO_ (return Nothing) $ do
293− r <- browse $ do
294− setOutHandler (const (return ()))
295− setAllowRedirects True
296− liftM snd . request . getRequest $ url
297− let content_type = lookupHeader HdrContentType (getHeaders r)
298− content <- liftM (Just . toStr . encode . toBS) . getResponseBody $ Right r
299− return $ liftM2 (,) content_type content
300−
301−toBS :: String -> B.ByteString
302−toBS = B.pack . map (toEnum . fromEnum)
303−
304−toStr :: B.ByteString -> String
305−toStr = map (toEnum . fromEnum) . B.unpack
306−
307294footnoteID :: Int -> String
308295footnoteID i = "n" ++ (show i)
309296
src/Text/Pandoc/Writers/ICML.hs+11−6
@@ -18,7 +18,7 @@ import Text.Pandoc.Definition
1818import Text.Pandoc.XML
1919import Text.Pandoc.Readers.TeXMath (texMathToInlines)
2020import Text.Pandoc.Writers.Shared
21−import Text.Pandoc.Shared (linesToPara, splitBy, fetchItem, warn)
21+import Text.Pandoc.Shared (linesToPara, splitBy, warn)
2222import Text.Pandoc.Options
2323import Text.Pandoc.Templates (renderTemplate')
2424import Text.Pandoc.Pretty
@@ -28,6 +28,8 @@ import Data.Text as Text (breakOnAll, pack)
2828import Control.Monad.State
2929import Network.URI (isURI)
3030import qualified Data.Set as Set
31+import Text.Pandoc.Free (runIO, PandocAction)
32+import qualified Text.Pandoc.Free as P
3133
3234type Style = [String]
3335type Hyperlink = [(Int, String)]
@@ -40,7 +42,7 @@ data WriterState = WriterState{
4042 , maxListDepth :: Int
4143 }
4244
43−type WS a = StateT WriterState IO a
45+type WS a = StateT WriterState PandocAction a
4446
4547defaultWriterState :: WriterState
4648defaultWriterState = WriterState{
@@ -121,10 +123,13 @@ subListParName = "subParagraph"
121123footnoteName = "Footnote"
122124citeName = "Cite"
123125
124−
125126-- | Convert Pandoc document to string in ICML format.
126127writeICML :: WriterOptions -> Pandoc -> IO String
127−writeICML opts (Pandoc meta blocks) = do
128+writeICML opts doc = runIO $ writeICMLPure opts doc
129+
130+-- | Convert Pandoc document to string in ICML format.
131+writeICMLPure :: WriterOptions -> Pandoc -> PandocAction String
132+writeICMLPure opts (Pandoc meta blocks) = do
128133 let colwidth = if writerWrapText opts == WrapAuto
129134 then Just $ writerColumns opts
130135 else Nothing
@@ -531,10 +536,10 @@ styleToStrAttr style =
531536-- | Assemble an ICML Image.
532537imageICML :: WriterOptions -> Style -> Attr -> Target -> WS Doc
533538imageICML opts style attr (src, _) = do
534− res <- liftIO $ fetchItem (writerSourceURL opts) src
539+ res <- lift $ P.fetchItem (writerSourceURL opts) src
535540 imgS <- case res of
536541 Left (_) -> do
537− liftIO $ warn $ "Could not find image `" ++ src ++ "', skipping..."
542+ lift $ P.warn $ "Could not find image `" ++ src ++ "', skipping..."
538543 return def
539544 Right (img, _) -> do
540545 case imageSize img of
src/Text/Pandoc/Writers/ODT.hs+45−26
@@ -28,8 +28,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2828
2929Conversion of 'Pandoc' documents to ODT.
3030-}
31−module Text.Pandoc.Writers.ODT ( writeODT ) where
32−import Data.IORef
31+module Text.Pandoc.Writers.ODT ( writeODTPure, writeODT ) where
3332import Data.List ( isPrefixOf )
3433import Data.Maybe ( fromMaybe )
3534import Text.XML.Light.Output
@@ -38,40 +37,60 @@ import qualified Data.ByteString.Lazy as B
3837import Text.Pandoc.UTF8 ( fromStringLazy )
3938import Codec.Archive.Zip
4039import Text.Pandoc.Options ( WriterOptions(..), WrapOption(..) )
41−import Text.Pandoc.Shared ( stringify, fetchItem', warn,
42− getDefaultReferenceODT )
40+import Text.Pandoc.Shared ( stringify )
4341import Text.Pandoc.ImageSize
4442import Text.Pandoc.MIME ( getMimeType, extensionFromMimeType )
4543import Text.Pandoc.Definition
4644import Text.Pandoc.Walk
4745import Text.Pandoc.Writers.Shared ( fixDisplayMath )
4846import Text.Pandoc.Writers.OpenDocument ( writeOpenDocument )
49−import Control.Monad (liftM)
47+import Control.Monad.State
5048import Text.Pandoc.XML
5149import Text.Pandoc.Pretty
5250import qualified Control.Exception as E
53−import Data.Time.Clock.POSIX ( getPOSIXTime )
5451import System.FilePath ( takeExtension, takeDirectory, (<.>))
52+import Text.Pandoc.Free ( PandocAction, runIO )
53+import qualified Text.Pandoc.Free as P
54+
55+data ODTState = ODTState { stEntries :: [Entry]
56+ }
57+
58+type O = StateT ODTState PandocAction
5559
5660-- | Produce an ODT file from a Pandoc document.
5761writeODT :: WriterOptions -- ^ Writer options
5862 -> Pandoc -- ^ Document to convert
5963 -> IO B.ByteString
60−writeODT opts doc@(Pandoc meta _) = do
64+writeODT opts doc = runIO $ writeODTPure opts doc
65+
66+writeODTPure :: WriterOptions
67+ -> Pandoc
68+ -> PandocAction B.ByteString
69+writeODTPure opts doc =
70+ let initState = ODTState{ stEntries = []
71+ }
72+ in
73+ evalStateT (pandocToODT opts doc) initState
74+
75+-- | Produce an ODT file from a Pandoc document.
76+pandocToODT :: WriterOptions -- ^ Writer options
77+ -> Pandoc -- ^ Document to convert
78+ -> O B.ByteString
79+pandocToODT opts doc@(Pandoc meta _) = do
6180 let datadir = writerUserDataDir opts
6281 let title = docTitle meta
6382 refArchive <-
6483 case writerReferenceODT opts of
65− Just f -> liftM toArchive $ B.readFile f
66− Nothing -> getDefaultReferenceODT datadir
84+ Just f -> liftM toArchive $ lift $ P.readFileLazy f
85+ Nothing -> lift $ P.getDefaultReferenceODT datadir
6786 -- handle formulas and pictures
68− picEntriesRef <- newIORef ([] :: [Entry])
69− doc' <- walkM (transformPicMath opts picEntriesRef) $ walk fixDisplayMath doc
87+ -- picEntriesRef <- P.newIORef ([] :: [Entry])
88+ doc' <- walkM (transformPicMath opts) $ walk fixDisplayMath doc
7089 let newContents = writeOpenDocument opts{writerWrapText = WrapNone} doc'
71− epochtime <- floor `fmap` getPOSIXTime
90+ epochtime <- floor `fmap` (lift P.getPOSIXTime)
7291 let contentEntry = toEntry "content.xml" epochtime
7392 $ fromStringLazy newContents
74− picEntries <- readIORef picEntriesRef
93+ picEntries <- gets stEntries
7594 let archive = foldr addEntryToArchive refArchive
7695 $ contentEntry : picEntries
7796 -- construct META-INF/manifest.xml based on archive
@@ -126,18 +145,18 @@ writeODT opts doc@(Pandoc meta _) = do
126145 return $ fromArchive archive''
127146
128147-- | transform both Image and Math elements
129−transformPicMath :: WriterOptions -> IORef [Entry] -> Inline -> IO Inline
130−transformPicMath opts entriesRef (Image attr@(id', cls, _) lab (src,t)) = do
131− res <- fetchItem' (writerMediaBag opts) (writerSourceURL opts) src
148+transformPicMath :: WriterOptions ->Inline -> O Inline
149+transformPicMath opts (Image attr@(id', cls, _) lab (src,t)) = do
150+ res <- lift $ P.fetchItem' (writerMediaBag opts) (writerSourceURL opts) src
132151 case res of
133152 Left (_ :: E.SomeException) -> do
134− warn $ "Could not find image `" ++ src ++ "', skipping..."
153+ lift $ P.warn $ "Could not find image `" ++ src ++ "', skipping..."
135154 return $ Emph lab
136155 Right (img, mbMimeType) -> do
137156 (ptX, ptY) <- case imageSize img of
138157 Right s -> return $ sizeInPoints s
139158 Left msg -> do
140− warn $ "Could not determine image size in `" ++
159+ lift $ P.warn $ "Could not determine image size in `" ++
141160 src ++ "': " ++ msg
142161 return (100, 100)
143162 let dims =
@@ -155,28 +174,28 @@ transformPicMath opts entriesRef (Image attr@(id', cls, _) lab (src,t)) = do
155174 Just dim -> Just $ Inch $ inInch opts dim
156175 Nothing -> Nothing
157176 let newattr = (id', cls, dims)
158− entries <- readIORef entriesRef
177+ entries <- gets stEntries
159178 let extension = fromMaybe (takeExtension $ takeWhile (/='?') src)
160179 (mbMimeType >>= extensionFromMimeType)
161180 let newsrc = "Pictures/" ++ show (length entries) <.> extension
162181 let toLazy = B.fromChunks . (:[])
163− epochtime <- floor `fmap` getPOSIXTime
182+ epochtime <- floor `fmap` (lift P.getPOSIXTime)
164183 let entry = toEntry newsrc epochtime $ toLazy img
165− modifyIORef entriesRef (entry:)
184+ modify $ \st -> st{ stEntries = entry : entries }
166185 return $ Image newattr lab (newsrc, t)
167−transformPicMath _ entriesRef (Math t math) = do
168− entries <- readIORef entriesRef
186+transformPicMath _ (Math t math) = do
187+ entries <- gets stEntries
169188 let dt = if t == InlineMath then DisplayInline else DisplayBlock
170189 case writeMathML dt <$> readTeX math of
171190 Left _ -> return $ Math t math
172191 Right r -> do
173192 let conf = useShortEmptyTags (const False) defaultConfigPP
174193 let mathml = ppcTopElement conf r
175− epochtime <- floor `fmap` getPOSIXTime
194+ epochtime <- floor `fmap` (lift $ P.getPOSIXTime)
176195 let dirname = "Formula-" ++ show (length entries) ++ "/"
177196 let fname = dirname ++ "content.xml"
178197 let entry = toEntry fname epochtime (fromStringLazy mathml)
179− modifyIORef entriesRef (entry:)
198+ modify $ \st -> st{ stEntries = entry : entries }
180199 return $ RawInline (Format "opendocument") $ render Nothing $
181200 inTags False "draw:frame" [("text:anchor-type",
182201 if t == DisplayMath
@@ -189,4 +208,4 @@ transformPicMath _ entriesRef (Math t math) = do
189208 , ("xlink:show", "embed")
190209 , ("xlink:actuate", "onLoad")]
191210
192−transformPicMath _ _ x = return x
211+transformPicMath _ x = return x
src/Text/Pandoc/Writers/RTF.hs+13−4
@@ -27,7 +27,10 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2727
2828Conversion of 'Pandoc' documents to RTF (rich text format).
2929-}
30−module Text.Pandoc.Writers.RTF ( writeRTF, writeRTFWithEmbeddedImages ) where
30+module Text.Pandoc.Writers.RTF ( writeRTF
31+ , writeRTFWithEmbeddedImages
32+ , writeRTFWithEmbeddedImagesPure
33+ ) where
3134import Text.Pandoc.Definition
3235import Text.Pandoc.Options
3336import Text.Pandoc.Shared
@@ -41,13 +44,15 @@ import qualified Data.ByteString as B
4144import qualified Data.Map as M
4245import Text.Printf ( printf )
4346import Text.Pandoc.ImageSize
47+import Text.Pandoc.Free (PandocAction, runIO)
48+import qualified Text.Pandoc.Free as P
4449
4550-- | Convert Image inlines into a raw RTF embedded image, read from a file,
4651-- or a MediaBag, or the internet.
4752-- If file not found or filetype not jpeg or png, leave the inline unchanged.
48−rtfEmbedImage :: WriterOptions -> Inline -> IO Inline
53+rtfEmbedImage :: WriterOptions -> Inline -> PandocAction Inline
4954rtfEmbedImage opts x@(Image attr _ (src,_)) = do
50− result <- fetchItem' (writerMediaBag opts) (writerSourceURL opts) src
55+ result <- P.fetchItem' (writerMediaBag opts) (writerSourceURL opts) src
5156 case result of
5257 Right (imgdata, Just mime)
5358 | mime == "image/jpeg" || mime == "image/png" -> do
@@ -58,7 +63,7 @@ rtfEmbedImage opts x@(Image attr _ (src,_)) = do
5863 _ -> error "Unknown file type"
5964 sizeSpec <- case imageSize imgdata of
6065 Left msg -> do
61− warn $ "Could not determine image size in `" ++
66+ P.warn $ "Could not determine image size in `" ++
6267 src ++ "': " ++ msg
6368 return ""
6469 Right sz -> return $ "\\picw" ++ show xpx ++
@@ -80,6 +85,10 @@ rtfEmbedImage _ x = return x
8085-- images embedded as encoded binary data.
8186writeRTFWithEmbeddedImages :: WriterOptions -> Pandoc -> IO String
8287writeRTFWithEmbeddedImages options doc =
88+ runIO $ writeRTF options `fmap` walkM (rtfEmbedImage options) doc
89+
90+writeRTFWithEmbeddedImagesPure :: WriterOptions -> Pandoc -> PandocAction String
91+writeRTFWithEmbeddedImagesPure options doc =
8392 writeRTF options `fmap` walkM (rtfEmbedImage options) doc
8493
8594-- | Convert Pandoc to a string in rich text format.