git.iko.soy/typeable/pandoc
on master help sign in
Text.Pandoc.Class: extract submodules PandocIO, PandocPure
commit2a042ff711ec9724a8b63b7cb4d29109379330c5 authorAlbert Krewinkel <albert@zeitkraut.de> dateSat 21 Mar 2020, 14:26 (7 years ago) parent66375f3
4 files changed+448−393
pandoc.cabal+2 src/Text/Pandoc/Class.hs+10−393 src/Text/Pandoc/Class/PandocIO.hs+218 src/Text/Pandoc/Class/PandocPure.hs+218
pandoc.cabal+2
@@ -570,6 +570,8 @@ library
570570 Text.Pandoc.App.OutputSettings,
571571 Text.Pandoc.Class.CommonState,
572572 Text.Pandoc.Class.PandocMonad,
573+ Text.Pandoc.Class.PandocIO,
574+ Text.Pandoc.Class.PandocPure,
573575 Text.Pandoc.Filter.JSON,
574576 Text.Pandoc.Filter.Lua,
575577 Text.Pandoc.Filter.Path,
src/Text/Pandoc/Class.hs+10−393
@@ -1,17 +1,6 @@
1−{-# LANGUAGE CPP #-}
2−{-# LANGUAGE PatternGuards #-}
3−{-# LANGUAGE DeriveFunctor #-}
4−{-# LANGUAGE MultiParamTypeClasses #-}
5−{-# LANGUAGE FlexibleInstances #-}
6−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
7−{-# LANGUAGE FlexibleContexts #-}
8−{-# LANGUAGE UndecidableInstances #-}
9−{-# LANGUAGE ScopedTypeVariables #-}
10−{-# LANGUAGE OverloadedStrings #-}
11−{-# LANGUAGE ViewPatterns #-}
121{- |
132 Module : Text.Pandoc.Class
14− Copyright : Copyright (C) 2016-17 Jesse Rosenthal, John MacFarlane
3+ Copyright : Copyright (C) 2016-2020 Jesse Rosenthal, John MacFarlane
154 License : GNU GPL, version 2 or above
165
176 Maintainer : Jesse Rosenthal <jrosenthal@jhu.edu>
@@ -25,388 +14,16 @@ whether they want conversions to perform IO operations (such as
2514reading include files or images).
2615-}
2716
28−module Text.Pandoc.Class ( PandocMonad(..)
29− , CommonState(..)
30− , PureState(..)
31− , getPureState
32− , getsPureState
33− , putPureState
34− , modifyPureState
35− , getPOSIXTime
36− , getZonedTime
37− , readFileFromDirs
38− , report
39− , setTrace
40− , setRequestHeader
41− , getLog
42− , setVerbosity
43− , getVerbosity
44− , getMediaBag
45− , setMediaBag
46− , insertMedia
47− , setUserDataDir
48− , getUserDataDir
49− , fetchItem
50− , getInputFiles
51− , setInputFiles
52− , getOutputFile
53− , setOutputFile
54− , setResourcePath
55− , getResourcePath
56− , PandocIO(..)
57− , PandocPure(..)
58− , FileTree
59− , FileInfo(..)
60− , addToFileTree
61− , insertInFileTree
62− , runIO
63− , runIOorExplode
64− , runPure
65− , readDefaultDataFile
66− , readDataFile
67− , fetchMediaResource
68− , fillMediaBag
69− , extractMedia
70− , toLang
71− , setTranslations
72− , translateTerm
73− , Translations
74− ) where
17+module Text.Pandoc.Class
18+ ( module Text.Pandoc.Class.CommonState
19+ , module Text.Pandoc.Class.PandocIO
20+ , module Text.Pandoc.Class.PandocMonad
21+ , module Text.Pandoc.Class.PandocPure
22+ , Translations
23+ ) where
7524
76−import Codec.Archive.Zip
77−import Control.Monad.Except
78−import Control.Monad.State.Strict
79−import Data.ByteString.Base64 (decodeLenient)
80−import Data.ByteString.Lazy (toChunks)
81−import Data.Default
82−import Data.Time (UTCTime)
83−import Data.Time.Clock.POSIX ( posixSecondsToUTCTime )
84−import Data.Time.LocalTime (TimeZone, utc)
85−import Data.Unique (hashUnique)
86−import Data.Word (Word8)
87−import Network.HTTP.Client
88− (httpLbs, responseBody, responseHeaders,
89− Request(port, host, requestHeaders), parseRequest, newManager)
90−import Network.HTTP.Client.Internal (addProxy)
91−import Network.HTTP.Client.TLS (tlsManagerSettings)
92−import Network.HTTP.Types.Header ( hContentType )
93−import Network.Socket (withSocketsDo)
94−import Network.URI ( unEscapeString )
95−import Prelude
96−import System.Directory (createDirectoryIfMissing, getDirectoryContents,
97− doesDirectoryExist)
98−import System.Environment (getEnv)
99−import System.FilePath ((</>), takeDirectory, normalise)
100−import System.FilePath.Glob (match, compile)
101−import System.IO (stderr)
102−import System.IO.Error
103−import System.Random (StdGen, next, mkStdGen)
10425import Text.Pandoc.Class.CommonState (CommonState (..))
10526import Text.Pandoc.Class.PandocMonad
106−import Text.Pandoc.Definition
107−import Text.Pandoc.Error
108−import Text.Pandoc.Logging
109−import Text.Pandoc.MediaBag (MediaBag, lookupMedia, mediaDirectory)
27+import Text.Pandoc.Class.PandocIO
28+import Text.Pandoc.Class.PandocPure
11029import Text.Pandoc.Translations (Translations)
111−import Text.Pandoc.Walk (walk)
112−import qualified Control.Exception as E
113−import qualified Data.ByteString as B
114−import qualified Data.ByteString.Lazy as BL
115−import qualified Data.CaseInsensitive as CI
116−import qualified Data.Map as M
117−import qualified Data.Text as T
118−import qualified Data.Time as IO (getCurrentTime)
119−import qualified Data.Time.LocalTime as IO (getCurrentTimeZone)
120−import qualified Data.Unique as IO (newUnique)
121−import qualified System.Directory as Directory
122−import qualified System.Directory as IO (getModificationTime)
123−import qualified System.Environment as IO (lookupEnv)
124−import qualified System.FilePath.Glob as IO (glob)
125−import qualified System.Random as IO (newStdGen)
126−import qualified Text.Pandoc.UTF8 as UTF8
127−#ifndef EMBED_DATA_FILES
128−import qualified Paths_pandoc as Paths
129−#endif
130−
131−-- | Evaluate a 'PandocIO' operation.
132−runIO :: PandocIO a -> IO (Either PandocError a)
133−runIO ma = flip evalStateT def $ runExceptT $ unPandocIO ma
134−
135−-- | Evaluate a 'PandocIO' operation, handling any errors
136−-- by exiting with an appropriate message and error status.
137−runIOorExplode :: PandocIO a -> IO a
138−runIOorExplode ma = runIO ma >>= handleError
139−
140−newtype PandocIO a = PandocIO {
141− unPandocIO :: ExceptT PandocError (StateT CommonState IO) a
142− } deriving ( MonadIO
143− , Functor
144− , Applicative
145− , Monad
146− , MonadError PandocError
147− )
148−
149−-- | Utility function to lift IO errors into 'PandocError's.
150−liftIOError :: (String -> IO a) -> String -> PandocIO a
151−liftIOError f u = do
152− res <- liftIO $ tryIOError $ f u
153− case res of
154− Left e -> throwError $ PandocIOError (T.pack u) e
155− Right r -> return r
156−
157−-- | Show potential IO errors to the user continuing execution anyway
158−logIOError :: IO () -> PandocIO ()
159−logIOError f = do
160− res <- liftIO $ tryIOError f
161− case res of
162− Left e -> report $ IgnoredIOError $ T.pack $ E.displayException e
163− Right _ -> pure ()
164−
165−instance PandocMonad PandocIO where
166− lookupEnv = fmap (fmap T.pack) . liftIO . IO.lookupEnv . T.unpack
167− getCurrentTime = liftIO IO.getCurrentTime
168− getCurrentTimeZone = liftIO IO.getCurrentTimeZone
169− newStdGen = liftIO IO.newStdGen
170− newUniqueHash = hashUnique <$> liftIO IO.newUnique
171−
172− openURL u
173− | Just u'' <- T.stripPrefix "data:" u = do
174− let mime = T.takeWhile (/=',') u''
175− let contents = UTF8.fromString $
176− unEscapeString $ T.unpack $ T.drop 1 $ T.dropWhile (/=',') u''
177− return (decodeLenient contents, Just mime)
178− | otherwise = do
179− let toReqHeader (n, v) = (CI.mk (UTF8.fromText n), UTF8.fromText v)
180− customHeaders <- map toReqHeader <$> getsCommonState stRequestHeaders
181− report $ Fetching u
182− res <- liftIO $ E.try $ withSocketsDo $ do
183− let parseReq = parseRequest
184− proxy <- tryIOError (getEnv "http_proxy")
185− let addProxy' x = case proxy of
186− Left _ -> return x
187− Right pr -> parseReq pr >>= \r ->
188− return (addProxy (host r) (port r) x)
189− req <- parseReq (T.unpack u) >>= addProxy'
190− let req' = req{requestHeaders = customHeaders ++ requestHeaders req}
191− resp <- newManager tlsManagerSettings >>= httpLbs req'
192− return (B.concat $ toChunks $ responseBody resp,
193− UTF8.toText `fmap` lookup hContentType (responseHeaders resp))
194−
195− case res of
196− Right r -> return r
197− Left e -> throwError $ PandocHttpError u e
198−
199− readFileLazy s = liftIOError BL.readFile s
200− readFileStrict s = liftIOError B.readFile s
201−
202− glob = liftIOError IO.glob
203− fileExists = liftIOError Directory.doesFileExist
204−#ifdef EMBED_DATA_FILES
205− getDataFileName = return
206−#else
207− getDataFileName = liftIOError Paths.getDataFileName
208−#endif
209− getModificationTime = liftIOError IO.getModificationTime
210− getCommonState = PandocIO $ lift get
211− putCommonState x = PandocIO $ lift $ put x
212− logOutput msg = liftIO $ do
213− UTF8.hPutStr stderr $
214− "[" ++ show (messageVerbosity msg) ++ "] "
215− alertIndent $ T.lines $ showLogMessage msg
216−
217−alertIndent :: [T.Text] -> IO ()
218−alertIndent [] = return ()
219−alertIndent (l:ls) = do
220− UTF8.hPutStrLn stderr $ T.unpack l
221− mapM_ go ls
222− where go l' = do UTF8.hPutStr stderr " "
223− UTF8.hPutStrLn stderr $ T.unpack l'
224−
225−-- | Extract media from the mediabag into a directory.
226−extractMedia :: FilePath -> Pandoc -> PandocIO Pandoc
227−extractMedia dir d = do
228− media <- getMediaBag
229− case [fp | (fp, _, _) <- mediaDirectory media] of
230− [] -> return d
231− fps -> do
232− mapM_ (writeMedia dir media) fps
233− return $ walk (adjustImagePath dir fps) d
234−
235−-- Write the contents of a media bag to a path.
236−writeMedia :: FilePath -> MediaBag -> FilePath -> PandocIO ()
237−writeMedia dir mediabag subpath = do
238− -- we join and split to convert a/b/c to a\b\c on Windows;
239− -- in zip containers all paths use /
240− let fullpath = dir </> unEscapeString (normalise subpath)
241− let mbcontents = lookupMedia subpath mediabag
242− case mbcontents of
243− Nothing -> throwError $ PandocResourceNotFound $ T.pack subpath
244− Just (_, bs) -> do
245− report $ Extracting $ T.pack fullpath
246− liftIOError (createDirectoryIfMissing True) (takeDirectory fullpath)
247− logIOError $ BL.writeFile fullpath bs
248−
249−adjustImagePath :: FilePath -> [FilePath] -> Inline -> Inline
250−adjustImagePath dir paths (Image attr lab (src, tit))
251− | T.unpack src `elem` paths = Image attr lab (T.pack dir <> "/" <> src, tit)
252−adjustImagePath _ _ x = x
253−
254−-- | The 'PureState' contains ersatz representations
255−-- of things that would normally be obtained through IO.
256−data PureState = PureState { stStdGen :: StdGen
257− , stWord8Store :: [Word8] -- should be
258− -- infinite,
259− -- i.e. [1..]
260− , stUniqStore :: [Int] -- should be
261− -- infinite and
262− -- contain every
263− -- element at most
264− -- once, e.g. [1..]
265− , stEnv :: [(T.Text, T.Text)]
266− , stTime :: UTCTime
267− , stTimeZone :: TimeZone
268− , stReferenceDocx :: Archive
269− , stReferencePptx :: Archive
270− , stReferenceODT :: Archive
271− , stFiles :: FileTree
272− , stUserDataFiles :: FileTree
273− , stCabalDataFiles :: FileTree
274− }
275−
276−instance Default PureState where
277− def = PureState { stStdGen = mkStdGen 1848
278− , stWord8Store = [1..]
279− , stUniqStore = [1..]
280− , stEnv = [("USER", "pandoc-user")]
281− , stTime = posixSecondsToUTCTime 0
282− , stTimeZone = utc
283− , stReferenceDocx = emptyArchive
284− , stReferencePptx = emptyArchive
285− , stReferenceODT = emptyArchive
286− , stFiles = mempty
287− , stUserDataFiles = mempty
288− , stCabalDataFiles = mempty
289− }
290−
291−
292−getPureState :: PandocPure PureState
293−getPureState = PandocPure $ lift $ lift get
294−
295−getsPureState :: (PureState -> a) -> PandocPure a
296−getsPureState f = f <$> getPureState
297−
298−putPureState :: PureState -> PandocPure ()
299−putPureState ps= PandocPure $ lift $ lift $ put ps
300−
301−modifyPureState :: (PureState -> PureState) -> PandocPure ()
302−modifyPureState f = PandocPure $ lift $ lift $ modify f
303−
304−
305−data FileInfo = FileInfo { infoFileMTime :: UTCTime
306− , infoFileContents :: B.ByteString
307− }
308−
309−newtype FileTree = FileTree {unFileTree :: M.Map FilePath FileInfo}
310− deriving (Semigroup, Monoid)
311−
312−getFileInfo :: FilePath -> FileTree -> Maybe FileInfo
313−getFileInfo fp tree =
314− M.lookup (makeCanonical fp) (unFileTree tree)
315−
316−-- | Add the specified file to the FileTree. If file
317−-- is a directory, add its contents recursively.
318−addToFileTree :: FileTree -> FilePath -> IO FileTree
319−addToFileTree tree fp = do
320− isdir <- doesDirectoryExist fp
321− if isdir
322− then do -- recursively add contents of directories
323− let isSpecial ".." = True
324− isSpecial "." = True
325− isSpecial _ = False
326− fs <- (map (fp </>) . filter (not . isSpecial)) <$> getDirectoryContents fp
327− foldM addToFileTree tree fs
328− else do
329− contents <- B.readFile fp
330− mtime <- IO.getModificationTime fp
331− return $ insertInFileTree fp FileInfo{ infoFileMTime = mtime
332− , infoFileContents = contents } tree
333−
334−-- | Insert an ersatz file into the 'FileTree'.
335−insertInFileTree :: FilePath -> FileInfo -> FileTree -> FileTree
336−insertInFileTree fp info (FileTree treemap) =
337− FileTree $ M.insert (makeCanonical fp) info treemap
338−
339−newtype PandocPure a = PandocPure {
340− unPandocPure :: ExceptT PandocError
341− (StateT CommonState (State PureState)) a
342− } deriving ( Functor
343− , Applicative
344− , Monad
345− , MonadError PandocError
346− )
347−
348−-- Run a 'PandocPure' operation.
349−runPure :: PandocPure a -> Either PandocError a
350−runPure x = flip evalState def $
351− flip evalStateT def $
352− runExceptT $
353− unPandocPure x
354−
355−instance PandocMonad PandocPure where
356− lookupEnv s = do
357− env <- getsPureState stEnv
358− return (lookup s env)
359−
360− getCurrentTime = getsPureState stTime
361−
362− getCurrentTimeZone = getsPureState stTimeZone
363−
364− newStdGen = do
365− g <- getsPureState stStdGen
366− let (_, nxtGen) = next g
367− modifyPureState $ \st -> st { stStdGen = nxtGen }
368− return g
369−
370− newUniqueHash = do
371− uniqs <- getsPureState stUniqStore
372− case uniqs of
373− u : us -> do
374− modifyPureState $ \st -> st { stUniqStore = us }
375− return u
376− _ -> throwError $ PandocShouldNeverHappenError
377− "uniq store ran out of elements"
378− openURL u = throwError $ PandocResourceNotFound u
379− readFileLazy fp = do
380− fps <- getsPureState stFiles
381− case infoFileContents <$> getFileInfo fp fps of
382− Just bs -> return (BL.fromStrict bs)
383− Nothing -> throwError $ PandocResourceNotFound $ T.pack fp
384− readFileStrict fp = do
385− fps <- getsPureState stFiles
386− case infoFileContents <$> getFileInfo fp fps of
387− Just bs -> return bs
388− Nothing -> throwError $ PandocResourceNotFound $ T.pack fp
389−
390− glob s = do
391− FileTree ftmap <- getsPureState stFiles
392− return $ filter (match (compile s)) $ M.keys ftmap
393−
394− fileExists fp = do
395− fps <- getsPureState stFiles
396− case getFileInfo fp fps of
397− Nothing -> return False
398− Just _ -> return True
399−
400− getDataFileName fp = return $ "data/" ++ fp
401−
402− getModificationTime fp = do
403− fps <- getsPureState stFiles
404− case infoFileMTime <$> getFileInfo fp fps of
405− Just tm -> return tm
406− Nothing -> throwError $ PandocIOError (T.pack fp)
407− (userError "Can't get modification time")
408−
409− getCommonState = PandocPure $ lift get
410− putCommonState x = PandocPure $ lift $ put x
411−
412− logOutput _msg = return ()
src/Text/Pandoc/Class/PandocIO.hsnew file+218
@@ -0,0 +1,218 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DeriveFunctor #-}
3+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+{-# LANGUAGE OverloadedStrings #-}
5+{- |
6+Module : Text.Pandoc.Class.PandocIO
7+Copyright : Copyright (C) 2016-2020 Jesse Rosenthal, John MacFarlane
8+License : GNU GPL, version 2 or above
9+
10+Maintainer : Jesse Rosenthal <jrosenthal@jhu.edu>
11+Stability : alpha
12+Portability : portable
13+
14+This module defines @'PandocIO'@, an IO-based instance of the
15+@'PandocMonad'@ type class. File, data, and network access all are run
16+using IO operators.
17+-}
18+module Text.Pandoc.Class.PandocIO
19+ ( getPOSIXTime
20+ , getZonedTime
21+ , readFileFromDirs
22+ , report
23+ , setTrace
24+ , setRequestHeader
25+ , getLog
26+ , setVerbosity
27+ , getVerbosity
28+ , getMediaBag
29+ , setMediaBag
30+ , insertMedia
31+ , setUserDataDir
32+ , getUserDataDir
33+ , fetchItem
34+ , getInputFiles
35+ , setInputFiles
36+ , getOutputFile
37+ , setOutputFile
38+ , setResourcePath
39+ , getResourcePath
40+ , PandocIO(..)
41+ , runIO
42+ , runIOorExplode
43+ , extractMedia
44+ ) where
45+
46+import Control.Monad.Except
47+import Control.Monad.State.Strict
48+import Data.ByteString.Base64 (decodeLenient)
49+import Data.ByteString.Lazy (toChunks)
50+import Data.Default
51+import Data.Text (Text)
52+import Data.Unique (hashUnique)
53+import Network.HTTP.Client
54+ (httpLbs, responseBody, responseHeaders,
55+ Request(port, host, requestHeaders), parseRequest, newManager)
56+import Network.HTTP.Client.Internal (addProxy)
57+import Network.HTTP.Client.TLS (tlsManagerSettings)
58+import Network.HTTP.Types.Header ( hContentType )
59+import Network.Socket (withSocketsDo)
60+import Network.URI ( unEscapeString )
61+import Prelude
62+import System.Directory (createDirectoryIfMissing)
63+import System.Environment (getEnv)
64+import System.FilePath ((</>), takeDirectory, normalise)
65+import System.IO (stderr)
66+import System.IO.Error
67+import Text.Pandoc.Class.CommonState (CommonState (..))
68+import Text.Pandoc.Class.PandocMonad
69+import Text.Pandoc.Definition
70+import Text.Pandoc.Error
71+import Text.Pandoc.Logging
72+import Text.Pandoc.MediaBag (MediaBag, lookupMedia, mediaDirectory)
73+import Text.Pandoc.Walk (walk)
74+import qualified Control.Exception as E
75+import qualified Data.ByteString as B
76+import qualified Data.ByteString.Lazy as BL
77+import qualified Data.CaseInsensitive as CI
78+import qualified Data.Text as T
79+import qualified Data.Time as IO (getCurrentTime)
80+import qualified Data.Time.LocalTime as IO (getCurrentTimeZone)
81+import qualified Data.Unique as IO (newUnique)
82+import qualified System.Directory as Directory
83+import qualified System.Directory as IO (getModificationTime)
84+import qualified System.Environment as IO (lookupEnv)
85+import qualified System.FilePath.Glob as IO (glob)
86+import qualified System.Random as IO (newStdGen)
87+import qualified Text.Pandoc.UTF8 as UTF8
88+#ifndef EMBED_DATA_FILES
89+import qualified Paths_pandoc as Paths
90+#endif
91+
92+-- | Evaluate a 'PandocIO' operation.
93+runIO :: PandocIO a -> IO (Either PandocError a)
94+runIO ma = flip evalStateT def $ runExceptT $ unPandocIO ma
95+
96+-- | Evaluate a 'PandocIO' operation, handling any errors
97+-- by exiting with an appropriate message and error status.
98+runIOorExplode :: PandocIO a -> IO a
99+runIOorExplode ma = runIO ma >>= handleError
100+
101+newtype PandocIO a = PandocIO {
102+ unPandocIO :: ExceptT PandocError (StateT CommonState IO) a
103+ } deriving ( MonadIO
104+ , Functor
105+ , Applicative
106+ , Monad
107+ , MonadError PandocError
108+ )
109+
110+-- | Utility function to lift IO errors into 'PandocError's.
111+liftIOError :: (String -> IO a) -> String -> PandocIO a
112+liftIOError f u = do
113+ res <- liftIO $ tryIOError $ f u
114+ case res of
115+ Left e -> throwError $ PandocIOError (T.pack u) e
116+ Right r -> return r
117+
118+-- | Show potential IO errors to the user continuing execution anyway
119+logIOError :: IO () -> PandocIO ()
120+logIOError f = do
121+ res <- liftIO $ tryIOError f
122+ case res of
123+ Left e -> report $ IgnoredIOError $ T.pack $ E.displayException e
124+ Right _ -> pure ()
125+
126+instance PandocMonad PandocIO where
127+ lookupEnv = fmap (fmap T.pack) . liftIO . IO.lookupEnv . T.unpack
128+ getCurrentTime = liftIO IO.getCurrentTime
129+ getCurrentTimeZone = liftIO IO.getCurrentTimeZone
130+ newStdGen = liftIO IO.newStdGen
131+ newUniqueHash = hashUnique <$> liftIO IO.newUnique
132+
133+ openURL u
134+ | Just u'' <- T.stripPrefix "data:" u = do
135+ let mime = T.takeWhile (/=',') u''
136+ let contents = UTF8.fromString $
137+ unEscapeString $ T.unpack $ T.drop 1 $ T.dropWhile (/=',') u''
138+ return (decodeLenient contents, Just mime)
139+ | otherwise = do
140+ let toReqHeader (n, v) = (CI.mk (UTF8.fromText n), UTF8.fromText v)
141+ customHeaders <- map toReqHeader <$> getsCommonState stRequestHeaders
142+ report $ Fetching u
143+ res <- liftIO $ E.try $ withSocketsDo $ do
144+ let parseReq = parseRequest
145+ proxy <- tryIOError (getEnv "http_proxy")
146+ let addProxy' x = case proxy of
147+ Left _ -> return x
148+ Right pr -> parseReq pr >>= \r ->
149+ return (addProxy (host r) (port r) x)
150+ req <- parseReq (T.unpack u) >>= addProxy'
151+ let req' = req{requestHeaders = customHeaders ++ requestHeaders req}
152+ resp <- newManager tlsManagerSettings >>= httpLbs req'
153+ return (B.concat $ toChunks $ responseBody resp,
154+ UTF8.toText `fmap` lookup hContentType (responseHeaders resp))
155+
156+ case res of
157+ Right r -> return r
158+ Left e -> throwError $ PandocHttpError u e
159+
160+ readFileLazy s = liftIOError BL.readFile s
161+ readFileStrict s = liftIOError B.readFile s
162+
163+ glob = liftIOError IO.glob
164+ fileExists = liftIOError Directory.doesFileExist
165+#ifdef EMBED_DATA_FILES
166+ getDataFileName = return
167+#else
168+ getDataFileName = liftIOError Paths.getDataFileName
169+#endif
170+ getModificationTime = liftIOError IO.getModificationTime
171+ getCommonState = PandocIO $ lift get
172+ putCommonState x = PandocIO $ lift $ put x
173+ logOutput msg = liftIO $ do
174+ UTF8.hPutStr stderr $
175+ "[" ++ show (messageVerbosity msg) ++ "] "
176+ alertIndent $ T.lines $ showLogMessage msg
177+
178+-- | Prints the list of lines to @stderr@, indenting every but the first
179+-- line by two spaces.
180+alertIndent :: [Text] -> IO ()
181+alertIndent [] = return ()
182+alertIndent (l:ls) = do
183+ UTF8.hPutStrLn stderr $ T.unpack l
184+ mapM_ go ls
185+ where go l' = do UTF8.hPutStr stderr " "
186+ UTF8.hPutStrLn stderr $ T.unpack l'
187+
188+-- | Extract media from the mediabag into a directory.
189+extractMedia :: FilePath -> Pandoc -> PandocIO Pandoc
190+extractMedia dir d = do
191+ media <- getMediaBag
192+ case [fp | (fp, _, _) <- mediaDirectory media] of
193+ [] -> return d
194+ fps -> do
195+ mapM_ (writeMedia dir media) fps
196+ return $ walk (adjustImagePath dir fps) d
197+
198+-- | Write the contents of a media bag to a path.
199+writeMedia :: FilePath -> MediaBag -> FilePath -> PandocIO ()
200+writeMedia dir mediabag subpath = do
201+ -- we join and split to convert a/b/c to a\b\c on Windows;
202+ -- in zip containers all paths use /
203+ let fullpath = dir </> unEscapeString (normalise subpath)
204+ let mbcontents = lookupMedia subpath mediabag
205+ case mbcontents of
206+ Nothing -> throwError $ PandocResourceNotFound $ T.pack subpath
207+ Just (_, bs) -> do
208+ report $ Extracting $ T.pack fullpath
209+ liftIOError (createDirectoryIfMissing True) (takeDirectory fullpath)
210+ logIOError $ BL.writeFile fullpath bs
211+
212+-- | If the given Inline element is an image with a @src@ path equal to
213+-- one in the list of @paths@, then prepends @dir@ to the image source;
214+-- returns the element unchanged otherwise.
215+adjustImagePath :: FilePath -> [FilePath] -> Inline -> Inline
216+adjustImagePath dir paths (Image attr lab (src, tit))
217+ | T.unpack src `elem` paths = Image attr lab (T.pack dir <> "/" <> src, tit)
218+adjustImagePath _ _ x = x
src/Text/Pandoc/Class/PandocPure.hsnew file+218
@@ -0,0 +1,218 @@
1+{-# LANGUAGE DeriveFunctor #-}
2+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
3+{-# LANGUAGE OverloadedStrings #-}
4+{- |
5+Module : Text.Pandoc.Class.Pure
6+Copyright : Copyright (C) 2016-2020 Jesse Rosenthal, John MacFarlane
7+License : GNU GPL, version 2 or above
8+
9+Maintainer : Jesse Rosenthal <jrosenthal@jhu.edu>
10+Stability : alpha
11+Portability : portable
12+
13+This module defines a pure instance 'PandocPure' of the @'PandocMonad'@
14+typeclass. This instance is useful for testing, or when all IO access is
15+prohibited for security reasons.
16+-}
17+module Text.Pandoc.Class.PandocPure
18+ ( PureState(..)
19+ , getPureState
20+ , getsPureState
21+ , putPureState
22+ , modifyPureState
23+ , PandocPure(..)
24+ , FileTree
25+ , FileInfo(..)
26+ , addToFileTree
27+ , insertInFileTree
28+ , runPure
29+ ) where
30+
31+import Codec.Archive.Zip
32+import Control.Monad.Except
33+import Control.Monad.State.Strict
34+import Data.Default
35+import Data.Text (Text)
36+import Data.Time (UTCTime)
37+import Data.Time.Clock.POSIX ( posixSecondsToUTCTime )
38+import Data.Time.LocalTime (TimeZone, utc)
39+import Data.Word (Word8)
40+import System.Directory (doesDirectoryExist, getDirectoryContents)
41+import System.FilePath ((</>))
42+import System.FilePath.Glob (match, compile)
43+import System.Random (StdGen, next, mkStdGen)
44+import Text.Pandoc.Class.CommonState (CommonState (..))
45+import Text.Pandoc.Class.PandocMonad
46+import Text.Pandoc.Error
47+import qualified Data.ByteString as B
48+import qualified Data.ByteString.Lazy as BL
49+import qualified Data.Map as M
50+import qualified Data.Text as T
51+import qualified System.Directory as Directory (getModificationTime)
52+
53+-- | The 'PureState' contains ersatz representations
54+-- of things that would normally be obtained through IO.
55+data PureState = PureState
56+ { stStdGen :: StdGen
57+ , stWord8Store :: [Word8] -- ^ should be infinite, i.e. [1..]
58+ , stUniqStore :: [Int] -- ^ should be infinite and contain every
59+ -- element at most once, e.g. [1..]
60+ , stEnv :: [(Text, Text)]
61+ , stTime :: UTCTime
62+ , stTimeZone :: TimeZone
63+ , stReferenceDocx :: Archive
64+ , stReferencePptx :: Archive
65+ , stReferenceODT :: Archive
66+ , stFiles :: FileTree
67+ , stUserDataFiles :: FileTree
68+ , stCabalDataFiles :: FileTree
69+ }
70+
71+instance Default PureState where
72+ def = PureState
73+ { stStdGen = mkStdGen 1848
74+ , stWord8Store = [1..]
75+ , stUniqStore = [1..]
76+ , stEnv = [("USER", "pandoc-user")]
77+ , stTime = posixSecondsToUTCTime 0
78+ , stTimeZone = utc
79+ , stReferenceDocx = emptyArchive
80+ , stReferencePptx = emptyArchive
81+ , stReferenceODT = emptyArchive
82+ , stFiles = mempty
83+ , stUserDataFiles = mempty
84+ , stCabalDataFiles = mempty
85+ }
86+
87+
88+-- | Retrieve the underlying state of the @'PandocPure'@ type.
89+getPureState :: PandocPure PureState
90+getPureState = PandocPure $ lift $ lift get
91+
92+-- | Retrieve a value from the underlying state of the @'PandocPure'@
93+-- type.
94+getsPureState :: (PureState -> a) -> PandocPure a
95+getsPureState f = f <$> getPureState
96+
97+-- | Set a new state for the @'PandocPure'@ type.
98+putPureState :: PureState -> PandocPure ()
99+putPureState ps= PandocPure $ lift $ lift $ put ps
100+
101+-- | Modify the underlying state of the @'PandocPure'@ type.
102+modifyPureState :: (PureState -> PureState) -> PandocPure ()
103+modifyPureState f = PandocPure $ lift $ lift $ modify f
104+
105+-- | Captures all file-level information necessary for a @'PandocMonad'@
106+-- conforming mock file system.
107+data FileInfo = FileInfo
108+ { infoFileMTime :: UTCTime
109+ , infoFileContents :: B.ByteString
110+ }
111+
112+-- | Basis of the mock file system used by @'PandocPure'@.
113+newtype FileTree = FileTree { unFileTree :: M.Map FilePath FileInfo }
114+ deriving (Semigroup, Monoid)
115+
116+-- | Retrieve @'FileInfo'@ of the given @'FilePath'@ from a
117+-- @'FileTree'@.
118+getFileInfo :: FilePath -> FileTree -> Maybe FileInfo
119+getFileInfo fp tree =
120+ M.lookup (makeCanonical fp) (unFileTree tree)
121+
122+-- | Add the specified file to the FileTree. If file
123+-- is a directory, add its contents recursively.
124+addToFileTree :: FileTree -> FilePath -> IO FileTree
125+addToFileTree tree fp = do
126+ isdir <- doesDirectoryExist fp
127+ if isdir
128+ then do -- recursively add contents of directories
129+ let isSpecial ".." = True
130+ isSpecial "." = True
131+ isSpecial _ = False
132+ fs <- map (fp </>) . filter (not . isSpecial) <$> getDirectoryContents fp
133+ foldM addToFileTree tree fs
134+ else do
135+ contents <- B.readFile fp
136+ mtime <- Directory.getModificationTime fp
137+ return $ insertInFileTree fp FileInfo{ infoFileMTime = mtime
138+ , infoFileContents = contents } tree
139+
140+-- | Insert an ersatz file into the 'FileTree'.
141+insertInFileTree :: FilePath -> FileInfo -> FileTree -> FileTree
142+insertInFileTree fp info (FileTree treemap) =
143+ FileTree $ M.insert (makeCanonical fp) info treemap
144+
145+newtype PandocPure a = PandocPure {
146+ unPandocPure :: ExceptT PandocError
147+ (StateT CommonState (State PureState)) a
148+ } deriving ( Functor
149+ , Applicative
150+ , Monad
151+ , MonadError PandocError
152+ )
153+
154+-- | Run a 'PandocPure' operation.
155+runPure :: PandocPure a -> Either PandocError a
156+runPure x = flip evalState def $
157+ flip evalStateT def $
158+ runExceptT $
159+ unPandocPure x
160+
161+instance PandocMonad PandocPure where
162+ lookupEnv s = do
163+ env <- getsPureState stEnv
164+ return (lookup s env)
165+
166+ getCurrentTime = getsPureState stTime
167+
168+ getCurrentTimeZone = getsPureState stTimeZone
169+
170+ newStdGen = do
171+ g <- getsPureState stStdGen
172+ let (_, nxtGen) = next g
173+ modifyPureState $ \st -> st { stStdGen = nxtGen }
174+ return g
175+
176+ newUniqueHash = do
177+ uniqs <- getsPureState stUniqStore
178+ case uniqs of
179+ u : us -> do
180+ modifyPureState $ \st -> st { stUniqStore = us }
181+ return u
182+ _ -> throwError $ PandocShouldNeverHappenError
183+ "uniq store ran out of elements"
184+ openURL u = throwError $ PandocResourceNotFound u
185+ readFileLazy fp = do
186+ fps <- getsPureState stFiles
187+ case infoFileContents <$> getFileInfo fp fps of
188+ Just bs -> return (BL.fromStrict bs)
189+ Nothing -> throwError $ PandocResourceNotFound $ T.pack fp
190+ readFileStrict fp = do
191+ fps <- getsPureState stFiles
192+ case infoFileContents <$> getFileInfo fp fps of
193+ Just bs -> return bs
194+ Nothing -> throwError $ PandocResourceNotFound $ T.pack fp
195+
196+ glob s = do
197+ FileTree ftmap <- getsPureState stFiles
198+ return $ filter (match (compile s)) $ M.keys ftmap
199+
200+ fileExists fp = do
201+ fps <- getsPureState stFiles
202+ case getFileInfo fp fps of
203+ Nothing -> return False
204+ Just _ -> return True
205+
206+ getDataFileName fp = return $ "data/" ++ fp
207+
208+ getModificationTime fp = do
209+ fps <- getsPureState stFiles
210+ case infoFileMTime <$> getFileInfo fp fps of
211+ Just tm -> return tm
212+ Nothing -> throwError $ PandocIOError (T.pack fp)
213+ (userError "Can't get modification time")
214+
215+ getCommonState = PandocPure $ lift get
216+ putCommonState x = PandocPure $ lift $ put x
217+
218+ logOutput _msg = return ()