git.iko.soy/typeable/pandoc
on master help sign in
Merge pull request #6211 from tarleb/lua-pandocerror

API change: create PandocLua type, use PandocError for exceptions

commit0d2b8e3fe1d6a27aac082be7711b7156783b3051 authorJohn MacFarlane <jgm@berkeley.edu> dateFri 17 Apr 2020, 18:02 (7 years ago) parent8f40b4b62cf21c
24 files changed+704−444
pandoc.cabal+9−3 src/Text/Pandoc/Class/IO.hs+231 src/Text/Pandoc/Class/PandocIO.hs+21−167 src/Text/Pandoc/Error.hs+2 src/Text/Pandoc/Filter/JSON.hs+3−3 src/Text/Pandoc/Filter/Lua.hs+4−5 src/Text/Pandoc/Lua.hs+1−2 src/Text/Pandoc/Lua/ErrorConversion.hs+61 src/Text/Pandoc/Lua/Filter.hs+13−7 src/Text/Pandoc/Lua/Init.hs+26−65 src/Text/Pandoc/Lua/Marshaling.hs+2−1 src/Text/Pandoc/Lua/Marshaling/AST.hs+10−5 src/Text/Pandoc/Lua/Marshaling/PandocError.hs+65 src/Text/Pandoc/Lua/Marshaling/Version.hs+2−2 src/Text/Pandoc/Lua/Module/MediaBag.hs+26−51 src/Text/Pandoc/Lua/Module/Pandoc.hs+17−15 src/Text/Pandoc/Lua/Module/Utils.hs+26−31 src/Text/Pandoc/Lua/Packages.hs+23−41 src/Text/Pandoc/Lua/PandocLua.hs+134 src/Text/Pandoc/Lua/Util.hs+7−17 src/Text/Pandoc/Writers/Custom.hs+9−18 stack.yaml+1 test/Tests/Lua.hs+8−7 test/lua/module/pandoc-types.lua+3−4
pandoc.cabal+9−3
@@ -412,9 +412,9 @@ library
412412 blaze-markup >= 0.8 && < 0.9,
413413 vector >= 0.10 && < 0.13,
414414 jira-wiki-markup >= 1.3 && < 1.4,
415− hslua >= 1.0.1 && < 1.2,
415+ hslua >= 1.1 && < 1.2,
416416 hslua-module-system >= 0.2 && < 0.3,
417− hslua-module-text >= 0.2 && < 0.3,
417+ hslua-module-text >= 0.2.1 && < 0.3,
418418 binary >= 0.5 && < 0.11,
419419 SHA >= 1.6 && < 1.7,
420420 haddock-library >= 1.8 && < 1.10,
@@ -571,6 +571,7 @@ library
571571 Text.Pandoc.App.Opt,
572572 Text.Pandoc.App.OutputSettings,
573573 Text.Pandoc.Class.CommonState,
574+ Text.Pandoc.Class.IO,
574575 Text.Pandoc.Class.PandocMonad,
575576 Text.Pandoc.Class.PandocIO,
576577 Text.Pandoc.Class.PandocPure,
@@ -611,6 +612,7 @@ library
611612 Text.Pandoc.Writers.Roff,
612613 Text.Pandoc.Writers.Powerpoint.Presentation,
613614 Text.Pandoc.Writers.Powerpoint.Output,
615+ Text.Pandoc.Lua.ErrorConversion,
614616 Text.Pandoc.Lua.Filter,
615617 Text.Pandoc.Lua.Global,
616618 Text.Pandoc.Lua.Init,
@@ -621,6 +623,7 @@ library
621623 Text.Pandoc.Lua.Marshaling.Context,
622624 Text.Pandoc.Lua.Marshaling.List,
623625 Text.Pandoc.Lua.Marshaling.MediaBag,
626+ Text.Pandoc.Lua.Marshaling.PandocError,
624627 Text.Pandoc.Lua.Marshaling.ReaderOptions,
625628 Text.Pandoc.Lua.Marshaling.Version,
626629 Text.Pandoc.Lua.Module.MediaBag,
@@ -629,6 +632,7 @@ library
629632 Text.Pandoc.Lua.Module.Types,
630633 Text.Pandoc.Lua.Module.Utils,
631634 Text.Pandoc.Lua.Packages,
635+ Text.Pandoc.Lua.PandocLua,
632636 Text.Pandoc.Lua.Util,
633637 Text.Pandoc.Lua.Walk,
634638 Text.Pandoc.CSS,
@@ -733,11 +737,13 @@ test-suite test-pandoc
733737 mtl >= 2.2 && < 2.3,
734738 bytestring >= 0.9 && < 0.11,
735739 base64-bytestring >= 0.1 && < 1.1,
740+ exceptions >= 0.8 && < 0.11,
736741 text >= 1.1.1.0 && < 1.3,
737742 time >= 1.5 && < 1.10,
738743 directory >= 1.2.3 && < 1.4,
744+ exceptions >= 0.8 && < 0.11,
739745 filepath >= 1.1 && < 1.5,
740− hslua >= 1.0 && < 1.2,
746+ hslua >= 1.1 && < 1.2,
741747 process >= 1.2.3 && < 1.7,
742748 temporary >= 1.1 && < 1.4,
743749 Diff >= 0.2 && < 0.5,
src/Text/Pandoc/Class/IO.hsnew file+231
@@ -0,0 +1,231 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+{- |
4+Module : Text.Pandoc.Class.IO
5+Copyright : Copyright (C) 2016-2020 Jesse Rosenthal, John MacFarlane
6+License : GNU GPL, version 2 or above
7+
8+Maintainer : Jesse Rosenthal <jrosenthal@jhu.edu>
9+Stability : alpha
10+Portability : portable
11+
12+Default ways to perform @'PandocMonad'@ actions in a @'MonadIO'@ type.
13+
14+These functions are used to make the @'PandocIO'@ type an instance of
15+@'PandocMonad'@, but can be reused for any other MonadIO-conforming
16+types.
17+-}
18+module Text.Pandoc.Class.IO
19+ ( fileExists
20+ , getCurrentTime
21+ , getCurrentTimeZone
22+ , getDataFileName
23+ , getModificationTime
24+ , glob
25+ , logOutput
26+ , logIOError
27+ , lookupEnv
28+ , newStdGen
29+ , newUniqueHash
30+ , openURL
31+ , readFileLazy
32+ , readFileStrict
33+ , extractMedia
34+ ) where
35+
36+import Control.Monad.Except (throwError)
37+import Control.Monad.IO.Class (MonadIO, liftIO)
38+import Data.ByteString.Base64 (decodeLenient)
39+import Data.ByteString.Lazy (toChunks)
40+import Data.Text (Text, pack, unpack)
41+import Data.Time (TimeZone, UTCTime)
42+import Data.Unique (hashUnique)
43+import Network.Connection (TLSSettings (TLSSettingsSimple))
44+import Network.HTTP.Client
45+ (httpLbs, responseBody, responseHeaders,
46+ Request(port, host, requestHeaders), parseRequest, newManager)
47+import Network.HTTP.Client.Internal (addProxy)
48+import Network.HTTP.Client.TLS (mkManagerSettings)
49+import Network.HTTP.Types.Header ( hContentType )
50+import Network.Socket (withSocketsDo)
51+import Network.URI (unEscapeString)
52+import System.Directory (createDirectoryIfMissing)
53+import System.Environment (getEnv)
54+import System.FilePath ((</>), takeDirectory, normalise)
55+import System.IO (stderr)
56+import System.IO.Error
57+import System.Random (StdGen)
58+import Text.Pandoc.Class.CommonState (CommonState (..))
59+import Text.Pandoc.Class.PandocMonad
60+ (PandocMonad, getsCommonState, getMediaBag, report)
61+import Text.Pandoc.Definition (Pandoc, Inline (Image))
62+import Text.Pandoc.Error (PandocError (..))
63+import Text.Pandoc.Logging (LogMessage (..), messageVerbosity, showLogMessage)
64+import Text.Pandoc.MIME (MimeType)
65+import Text.Pandoc.MediaBag (MediaBag, lookupMedia, mediaDirectory)
66+import Text.Pandoc.Walk (walk)
67+import qualified Control.Exception as E
68+import qualified Data.ByteString as B
69+import qualified Data.ByteString.Lazy as BL
70+import qualified Data.CaseInsensitive as CI
71+import qualified Data.Text as T
72+import qualified Data.Time
73+import qualified Data.Time.LocalTime
74+import qualified Data.Unique
75+import qualified System.Directory
76+import qualified System.Environment as Env
77+import qualified System.FilePath.Glob
78+import qualified System.Random
79+import qualified Text.Pandoc.UTF8 as UTF8
80+#ifndef EMBED_DATA_FILES
81+import qualified Paths_pandoc as Paths
82+#endif
83+
84+-- | Utility function to lift IO errors into 'PandocError's.
85+liftIOError :: (PandocMonad m, MonadIO m) => (String -> IO a) -> String -> m a
86+liftIOError f u = do
87+ res <- liftIO $ tryIOError $ f u
88+ case res of
89+ Left e -> throwError $ PandocIOError (pack u) e
90+ Right r -> return r
91+
92+-- | Show potential IO errors to the user continuing execution anyway
93+logIOError :: (PandocMonad m, MonadIO m) => IO () -> m ()
94+logIOError f = do
95+ res <- liftIO $ tryIOError f
96+ case res of
97+ Left e -> report $ IgnoredIOError $ pack $ E.displayException e
98+ Right _ -> pure ()
99+
100+-- | Lookup an environment variable in the programs environment.
101+lookupEnv :: MonadIO m => Text -> m (Maybe Text)
102+lookupEnv = fmap (fmap pack) . liftIO . Env.lookupEnv . unpack
103+
104+-- | Get the current (UTC) time.
105+getCurrentTime :: MonadIO m => m UTCTime
106+getCurrentTime = liftIO Data.Time.getCurrentTime
107+
108+-- | Get the locale's time zone.
109+getCurrentTimeZone :: MonadIO m => m TimeZone
110+getCurrentTimeZone = liftIO Data.Time.LocalTime.getCurrentTimeZone
111+
112+-- | Return a new generator for random numbers.
113+newStdGen :: MonadIO m => m StdGen
114+newStdGen = liftIO System.Random.newStdGen
115+
116+-- | Return a new unique integer.
117+newUniqueHash :: MonadIO m => m Int
118+newUniqueHash = hashUnique <$> liftIO Data.Unique.newUnique
119+
120+openURL :: (PandocMonad m, MonadIO m) => Text -> m (B.ByteString, Maybe MimeType)
121+openURL u
122+ | Just u'' <- T.stripPrefix "data:" u = do
123+ let mime = T.takeWhile (/=',') u''
124+ let contents = UTF8.fromString $
125+ unEscapeString $ T.unpack $ T.drop 1 $ T.dropWhile (/=',') u''
126+ return (decodeLenient contents, Just mime)
127+ | otherwise = do
128+ let toReqHeader (n, v) = (CI.mk (UTF8.fromText n), UTF8.fromText v)
129+ customHeaders <- map toReqHeader <$> getsCommonState stRequestHeaders
130+ disableCertificateValidation <- getsCommonState stNoCheckCertificate
131+ report $ Fetching u
132+ res <- liftIO $ E.try $ withSocketsDo $ do
133+ let parseReq = parseRequest
134+ proxy <- tryIOError (getEnv "http_proxy")
135+ let addProxy' x = case proxy of
136+ Left _ -> return x
137+ Right pr -> parseReq pr >>= \r ->
138+ return (addProxy (host r) (port r) x)
139+ req <- parseReq (unpack u) >>= addProxy'
140+ let req' = req{requestHeaders = customHeaders ++ requestHeaders req}
141+ let tlsSimple = TLSSettingsSimple disableCertificateValidation False False
142+ let tlsManagerSettings = mkManagerSettings tlsSimple Nothing
143+ resp <- newManager tlsManagerSettings >>= httpLbs req'
144+ return (B.concat $ toChunks $ responseBody resp,
145+ UTF8.toText `fmap` lookup hContentType (responseHeaders resp))
146+
147+ case res of
148+ Right r -> return r
149+ Left e -> throwError $ PandocHttpError u e
150+
151+-- | Read the lazy ByteString contents from a file path, raising an error on
152+-- failure.
153+readFileLazy :: (PandocMonad m, MonadIO m) => FilePath -> m BL.ByteString
154+readFileLazy s = liftIOError BL.readFile s
155+
156+-- | Read the strict ByteString contents from a file path,
157+-- raising an error on failure.
158+readFileStrict :: (PandocMonad m, MonadIO m) => FilePath -> m B.ByteString
159+readFileStrict s = liftIOError B.readFile s
160+
161+-- | Return a list of paths that match a glob, relative to the working
162+-- directory. See 'System.FilePath.Glob' for the glob syntax.
163+glob :: (PandocMonad m, MonadIO m) => String -> m [FilePath]
164+glob = liftIOError System.FilePath.Glob.glob
165+
166+-- | Returns True if file exists.
167+fileExists :: (PandocMonad m, MonadIO m) => FilePath -> m Bool
168+fileExists = liftIOError System.Directory.doesFileExist
169+
170+-- | Returns the path of data file.
171+getDataFileName :: (PandocMonad m, MonadIO m) => FilePath -> m FilePath
172+#ifdef EMBED_DATA_FILES
173+getDataFileName = return
174+#else
175+getDataFileName = liftIOError Paths.getDataFileName
176+#endif
177+
178+-- | Return the modification time of a file.
179+getModificationTime :: (PandocMonad m, MonadIO m) => FilePath -> m UTCTime
180+getModificationTime = liftIOError System.Directory.getModificationTime
181+
182+-- | Output a log message.
183+logOutput :: (PandocMonad m, MonadIO m) => LogMessage -> m ()
184+logOutput msg = liftIO $ do
185+ UTF8.hPutStr stderr $
186+ "[" ++ show (messageVerbosity msg) ++ "] "
187+ alertIndent $ T.lines $ showLogMessage msg
188+
189+-- | Prints the list of lines to @stderr@, indenting every but the first
190+-- line by two spaces.
191+alertIndent :: [Text] -> IO ()
192+alertIndent [] = return ()
193+alertIndent (l:ls) = do
194+ UTF8.hPutStrLn stderr $ unpack l
195+ mapM_ go ls
196+ where go l' = do UTF8.hPutStr stderr " "
197+ UTF8.hPutStrLn stderr $ unpack l'
198+
199+-- | Extract media from the mediabag into a directory.
200+extractMedia :: (PandocMonad m, MonadIO m) => FilePath -> Pandoc -> m Pandoc
201+extractMedia dir d = do
202+ media <- getMediaBag
203+ case [fp | (fp, _, _) <- mediaDirectory media] of
204+ [] -> return d
205+ fps -> do
206+ mapM_ (writeMedia dir media) fps
207+ return $ walk (adjustImagePath dir fps) d
208+
209+-- | Write the contents of a media bag to a path.
210+writeMedia :: (PandocMonad m, MonadIO m)
211+ => FilePath -> MediaBag -> FilePath
212+ -> m ()
213+writeMedia dir mediabag subpath = do
214+ -- we join and split to convert a/b/c to a\b\c on Windows;
215+ -- in zip containers all paths use /
216+ let fullpath = dir </> unEscapeString (normalise subpath)
217+ let mbcontents = lookupMedia subpath mediabag
218+ case mbcontents of
219+ Nothing -> throwError $ PandocResourceNotFound $ pack subpath
220+ Just (_, bs) -> do
221+ report $ Extracting $ pack fullpath
222+ liftIOError (createDirectoryIfMissing True) (takeDirectory fullpath)
223+ logIOError $ BL.writeFile fullpath bs
224+
225+-- | If the given Inline element is an image with a @src@ path equal to
226+-- one in the list of @paths@, then prepends @dir@ to the image source;
227+-- returns the element unchanged otherwise.
228+adjustImagePath :: FilePath -> [FilePath] -> Inline -> Inline
229+adjustImagePath dir paths (Image attr lab (src, tit))
230+ | unpack src `elem` paths = Image attr lab (pack dir <> "/" <> src, tit)
231+adjustImagePath _ _ x = x
src/Text/Pandoc/Class/PandocIO.hs+21−167
@@ -1,7 +1,5 @@
1−{-# LANGUAGE CPP #-}
21{-# LANGUAGE DeriveFunctor #-}
32{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4−{-# LANGUAGE OverloadedStrings #-}
53{- |
64Module : Text.Pandoc.Class.PandocIO
75Copyright : Copyright (C) 2016-2020 Jesse Rosenthal, John MacFarlane
@@ -16,79 +14,21 @@ This module defines @'PandocIO'@, an IO-based instance of the
1614using IO operators.
1715-}
1816module 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(..)
17+ ( PandocIO(..)
4118 , runIO
4219 , runIOorExplode
4320 , extractMedia
4421 ) where
4522
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 (mkManagerSettings)
58−import Network.Connection (TLSSettings (..))
59−import Network.HTTP.Types.Header ( hContentType )
60−import Network.Socket (withSocketsDo)
61−import Network.URI ( unEscapeString )
62−import Prelude
63−import System.Directory (createDirectoryIfMissing)
64−import System.Environment (getEnv)
65−import System.FilePath ((</>), takeDirectory, normalise)
66−import System.IO (stderr)
67−import System.IO.Error
23+import Control.Monad.Except (ExceptT, MonadError, runExceptT)
24+import Control.Monad.IO.Class (MonadIO)
25+import Control.Monad.State (StateT, evalStateT, lift, get, put)
26+import Data.Default (Default (def))
6827import Text.Pandoc.Class.CommonState (CommonState (..))
6928import Text.Pandoc.Class.PandocMonad
7029import Text.Pandoc.Definition
7130import Text.Pandoc.Error
72−import Text.Pandoc.Logging
73−import Text.Pandoc.MediaBag (MediaBag, lookupMedia, mediaDirectory)
74−import Text.Pandoc.Walk (walk)
75−import qualified Control.Exception as E
76−import qualified Data.ByteString as B
77−import qualified Data.ByteString.Lazy as BL
78−import qualified Data.CaseInsensitive as CI
79−import qualified Data.Text as T
80−import qualified Data.Time as IO (getCurrentTime)
81−import qualified Data.Time.LocalTime as IO (getCurrentTimeZone)
82−import qualified Data.Unique as IO (newUnique)
83−import qualified System.Directory as Directory
84−import qualified System.Directory as IO (getModificationTime)
85−import qualified System.Environment as IO (lookupEnv)
86−import qualified System.FilePath.Glob as IO (glob)
87−import qualified System.Random as IO (newStdGen)
88−import qualified Text.Pandoc.UTF8 as UTF8
89−#ifndef EMBED_DATA_FILES
90−import qualified Paths_pandoc as Paths
91−#endif
31+import qualified Text.Pandoc.Class.IO as IO
9232
9333-- | Evaluate a 'PandocIO' operation.
9434runIO :: PandocIO a -> IO (Either PandocError a)
@@ -108,113 +48,27 @@ newtype PandocIO a = PandocIO {
10848 , MonadError PandocError
10949 )
11050
111−-- | Utility function to lift IO errors into 'PandocError's.
112−liftIOError :: (String -> IO a) -> String -> PandocIO a
113−liftIOError f u = do
114− res <- liftIO $ tryIOError $ f u
115− case res of
116− Left e -> throwError $ PandocIOError (T.pack u) e
117− Right r -> return r
118−
119−-- | Show potential IO errors to the user continuing execution anyway
120−logIOError :: IO () -> PandocIO ()
121−logIOError f = do
122− res <- liftIO $ tryIOError f
123− case res of
124− Left e -> report $ IgnoredIOError $ T.pack $ E.displayException e
125− Right _ -> pure ()
126−
12751instance PandocMonad PandocIO where
128− lookupEnv = fmap (fmap T.pack) . liftIO . IO.lookupEnv . T.unpack
129− getCurrentTime = liftIO IO.getCurrentTime
130− getCurrentTimeZone = liftIO IO.getCurrentTimeZone
131− newStdGen = liftIO IO.newStdGen
132− newUniqueHash = hashUnique <$> liftIO IO.newUnique
133−
134− openURL u
135− | Just u'' <- T.stripPrefix "data:" u = do
136− let mime = T.takeWhile (/=',') u''
137− let contents = UTF8.fromString $
138− unEscapeString $ T.unpack $ T.drop 1 $ T.dropWhile (/=',') u''
139− return (decodeLenient contents, Just mime)
140− | otherwise = do
141− let toReqHeader (n, v) = (CI.mk (UTF8.fromText n), UTF8.fromText v)
142− customHeaders <- map toReqHeader <$> getsCommonState stRequestHeaders
143− disableCertificateValidation <- getsCommonState stNoCheckCertificate
144− report $ Fetching u
145− res <- liftIO $ E.try $ withSocketsDo $ do
146− let parseReq = parseRequest
147− proxy <- tryIOError (getEnv "http_proxy")
148− let addProxy' x = case proxy of
149− Left _ -> return x
150− Right pr -> parseReq pr >>= \r ->
151− return (addProxy (host r) (port r) x)
152− req <- parseReq (T.unpack u) >>= addProxy'
153− let req' = req{requestHeaders = customHeaders ++ requestHeaders req}
154− resp <- newManager (mkManagerSettings (TLSSettingsSimple disableCertificateValidation False False) Nothing) >>= httpLbs req'
155− return (B.concat $ toChunks $ responseBody resp,
156− UTF8.toText `fmap` lookup hContentType (responseHeaders resp))
52+ lookupEnv = IO.lookupEnv
53+ getCurrentTime = IO.getCurrentTime
54+ getCurrentTimeZone = IO.getCurrentTimeZone
55+ newStdGen = IO.newStdGen
56+ newUniqueHash = IO.newUniqueHash
15757
158− case res of
159− Right r -> return r
160− Left e -> throwError $ PandocHttpError u e
58+ openURL = IO.openURL
59+ readFileLazy = IO.readFileLazy
60+ readFileStrict = IO.readFileStrict
16161
162− readFileLazy s = liftIOError BL.readFile s
163− readFileStrict s = liftIOError B.readFile s
62+ glob = IO.glob
63+ fileExists = IO.fileExists
64+ getDataFileName = IO.getDataFileName
65+ getModificationTime = IO.getModificationTime
16466
165− glob = liftIOError IO.glob
166− fileExists = liftIOError Directory.doesFileExist
167−#ifdef EMBED_DATA_FILES
168− getDataFileName = return
169−#else
170− getDataFileName = liftIOError Paths.getDataFileName
171−#endif
172− getModificationTime = liftIOError IO.getModificationTime
17367 getCommonState = PandocIO $ lift get
174− putCommonState x = PandocIO $ lift $ put x
175− logOutput msg = liftIO $ do
176− UTF8.hPutStr stderr $
177− "[" ++ show (messageVerbosity msg) ++ "] "
178− alertIndent $ T.lines $ showLogMessage msg
68+ putCommonState = PandocIO . lift . put
17969
180−-- | Prints the list of lines to @stderr@, indenting every but the first
181−-- line by two spaces.
182−alertIndent :: [Text] -> IO ()
183−alertIndent [] = return ()
184−alertIndent (l:ls) = do
185− UTF8.hPutStrLn stderr $ T.unpack l
186− mapM_ go ls
187− where go l' = do UTF8.hPutStr stderr " "
188− UTF8.hPutStrLn stderr $ T.unpack l'
70+ logOutput = IO.logOutput
18971
19072-- | Extract media from the mediabag into a directory.
19173extractMedia :: FilePath -> Pandoc -> PandocIO Pandoc
192−extractMedia dir d = do
193− media <- getMediaBag
194− case [fp | (fp, _, _) <- mediaDirectory media] of
195− [] -> return d
196− fps -> do
197− mapM_ (writeMedia dir media) fps
198− return $ walk (adjustImagePath dir fps) d
199−
200−-- | Write the contents of a media bag to a path.
201−writeMedia :: FilePath -> MediaBag -> FilePath -> PandocIO ()
202−writeMedia dir mediabag subpath = do
203− -- we join and split to convert a/b/c to a\b\c on Windows;
204− -- in zip containers all paths use /
205− let fullpath = dir </> unEscapeString (normalise subpath)
206− let mbcontents = lookupMedia subpath mediabag
207− case mbcontents of
208− Nothing -> throwError $ PandocResourceNotFound $ T.pack subpath
209− Just (_, bs) -> do
210− report $ Extracting $ T.pack fullpath
211− liftIOError (createDirectoryIfMissing True) (takeDirectory fullpath)
212− logIOError $ BL.writeFile fullpath bs
213−
214−-- | If the given Inline element is an image with a @src@ path equal to
215−-- one in the list of @paths@, then prepends @dir@ to the image source;
216−-- returns the element unchanged otherwise.
217−adjustImagePath :: FilePath -> [FilePath] -> Inline -> Inline
218−adjustImagePath dir paths (Image attr lab (src, tit))
219− | T.unpack src `elem` paths = Image attr lab (T.pack dir <> "/" <> src, tit)
220−adjustImagePath _ _ x = x
74+extractMedia = IO.extractMedia
src/Text/Pandoc/Error.hs+2
@@ -47,6 +47,7 @@ data PandocError = PandocIOError Text IOError
4747 | PandocPDFProgramNotFoundError Text
4848 | PandocPDFError Text
4949 | PandocFilterError Text Text
50+ | PandocLuaError Text
5051 | PandocCouldNotFindDataFileError Text
5152 | PandocResourceNotFound Text
5253 | PandocTemplateError Text
@@ -100,6 +101,7 @@ handleError (Left e) =
100101 PandocPDFError logmsg -> err 43 $ "Error producing PDF.\n" <> logmsg
101102 PandocFilterError filtername msg -> err 83 $ "Error running filter " <>
102103 filtername <> ":\n" <> msg
104+ PandocLuaError msg -> err 84 $ "Error running Lua:\n" <> msg
103105 PandocCouldNotFindDataFileError fn -> err 97 $
104106 "Could not find data file " <> fn
105107 PandocResourceNotFound fn -> err 99 $
src/Text/Pandoc/Filter/JSON.hs+3−3
@@ -23,7 +23,6 @@ import System.Directory (executable, doesFileExist, findExecutable,
2323import System.Environment (getEnvironment)
2424import System.Exit (ExitCode (..))
2525import System.FilePath ((</>), takeExtension)
26−import Text.Pandoc.Class.PandocIO (PandocIO)
2726import Text.Pandoc.Error (PandocError (PandocFilterError))
2827import Text.Pandoc.Definition (Pandoc)
2928import Text.Pandoc.Options (ReaderOptions)
@@ -32,11 +31,12 @@ import Text.Pandoc.Shared (pandocVersion, tshow)
3231import qualified Control.Exception as E
3332import qualified Text.Pandoc.UTF8 as UTF8
3433
35−apply :: ReaderOptions
34+apply :: MonadIO m
35+ => ReaderOptions
3636 -> [String]
3737 -> FilePath
3838 -> Pandoc
39− -> PandocIO Pandoc
39+ -> m Pandoc
4040apply ropts args f = liftIO . externalFilter ropts f args
4141
4242externalFilter :: MonadIO m
src/Text/Pandoc/Filter/Lua.hs+4−5
@@ -17,8 +17,7 @@ import qualified Data.Text as T
1717import Text.Pandoc.Class.PandocIO (PandocIO)
1818import Text.Pandoc.Definition (Pandoc)
1919import Text.Pandoc.Error (PandocError (PandocFilterError))
20−import Text.Pandoc.Lua (Global (..), LuaException (..),
21− runLua, runFilterFile, setGlobals)
20+import Text.Pandoc.Lua (Global (..), runLua, runFilterFile, setGlobals)
2221import Text.Pandoc.Options (ReaderOptions)
2322
2423-- | Run the Lua filter in @filterPath@ for a transformation to the
@@ -40,7 +39,7 @@ apply ropts args fp doc = do
4039 ]
4140 runFilterFile fp doc
4241
43−forceResult :: FilePath -> Either LuaException Pandoc -> PandocIO Pandoc
42+forceResult :: FilePath -> Either PandocError Pandoc -> PandocIO Pandoc
4443forceResult fp eitherResult = case eitherResult of
45− Right x -> return x
46− Left (LuaException s) -> throw (PandocFilterError (T.pack fp) s)
44+ Right x -> return x
45+ Left err -> throw (PandocFilterError (T.pack fp) (T.pack $ show err))
src/Text/Pandoc/Lua.hs+1−2
@@ -10,7 +10,6 @@ Running pandoc Lua filters.
1010-}
1111module Text.Pandoc.Lua
1212 ( runLua
13− , LuaException (..)
1413 -- * Lua globals
1514 , Global (..)
1615 , setGlobals
@@ -20,5 +19,5 @@ module Text.Pandoc.Lua
2019
2120import Text.Pandoc.Lua.Filter (runFilterFile)
2221import Text.Pandoc.Lua.Global (Global (..), setGlobals)
23−import Text.Pandoc.Lua.Init (LuaException (..), runLua)
22+import Text.Pandoc.Lua.Init (runLua)
2423import Text.Pandoc.Lua.Marshaling ()
src/Text/Pandoc/Lua/ErrorConversion.hsnew file+61
@@ -0,0 +1,61 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE RankNTypes #-}
3+{-# LANGUAGE ScopedTypeVariables #-}
4+{- |
5+ Module : Text.Pandoc.Lua.ErrorConversion
6+ Copyright : © 2020 Albert Krewinkel
7+ License : GNU GPL, version 2 or above
8+
9+ Maintainer : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
10+ Stability : alpha
11+
12+Define how Lua errors are converted into @'PandocError'@ Haskell
13+exceptions, and /vice versa/.
14+-}
15+module Text.Pandoc.Lua.ErrorConversion
16+ ( errorConversion
17+ ) where
18+
19+import Foreign.Lua (Lua (..), NumResults)
20+import Text.Pandoc.Error (PandocError (PandocLuaError))
21+import Text.Pandoc.Lua.Marshaling.PandocError (pushPandocError, peekPandocError)
22+
23+import qualified Control.Monad.Catch as Catch
24+import qualified Data.Text as T
25+import qualified Foreign.Lua as Lua
26+
27+-- | Conversions between Lua errors and Haskell exceptions, assuming
28+-- that all exceptions are of type @'PandocError'@.
29+errorConversion :: Lua.ErrorConversion
30+errorConversion = Lua.ErrorConversion
31+ { Lua.addContextToException = addContextToException
32+ , Lua.alternative = alternative
33+ , Lua.errorToException = errorToException
34+ , Lua.exceptionToError = exceptionToError
35+ }
36+
37+-- | Convert a Lua error, which must be at the top of the stack, into a
38+-- @'PandocError'@, popping the value from the stack.
39+errorToException :: forall a . Lua.State -> IO a
40+errorToException l = Lua.unsafeRunWith l $ do
41+ err <- peekPandocError Lua.stackTop
42+ Lua.pop 1
43+ Catch.throwM err
44+
45+-- | Try the first op -- if it doesn't succeed, run the second.
46+alternative :: forall a . Lua a -> Lua a -> Lua a
47+alternative x y = Catch.try x >>= \case
48+ Left (_ :: PandocError) -> y
49+ Right x' -> return x'
50+
51+-- | Add more context to an error
52+addContextToException :: forall a . String -> Lua a -> Lua a
53+addContextToException ctx op = op `Catch.catch` \case
54+ PandocLuaError msg -> Catch.throwM $ PandocLuaError (T.pack ctx <> msg)
55+ e -> Catch.throwM e
56+
57+-- | Catch a @'PandocError'@ exception and raise it as a Lua error.
58+exceptionToError :: Lua NumResults -> Lua NumResults
59+exceptionToError op = op `Catch.catch` \e -> do
60+ pushPandocError e
61+ Lua.error
src/Text/Pandoc/Lua/Filter.hs+13−7
@@ -18,14 +18,15 @@ module Text.Pandoc.Lua.Filter ( LuaFilterFunction
1818 ) where
1919import Control.Applicative ((<|>))
2020import Control.Monad (mplus, (>=>))
21−import Control.Monad.Catch (finally)
21+import Control.Monad.Catch (finally, try)
2222import Data.Data (Data, DataType, dataTypeConstrs, dataTypeName, dataTypeOf,
2323 showConstr, toConstr, tyconUQname)
2424import Data.Foldable (foldrM)
2525import Data.Map (Map)
2626import Data.Maybe (fromMaybe)
27−import Foreign.Lua (Lua, Peekable, Pushable)
27+import Foreign.Lua (Lua, Peekable, Pushable, StackIndex)
2828import Text.Pandoc.Definition
29+import Text.Pandoc.Error (PandocError)
2930import Text.Pandoc.Lua.Marshaling ()
3031import Text.Pandoc.Lua.Marshaling.List (List (..))
3132import Text.Pandoc.Lua.Walk (SingletonsList (..))
@@ -102,7 +103,7 @@ elementOrList x = do
102103 if elementUnchanged
103104 then [x] <$ Lua.pop 1
104105 else do
105− mbres <- Lua.peekEither topOfStack
106+ mbres <- peekEither topOfStack
106107 case mbres of
107108 Right res -> [res] <$ Lua.pop 1
108109 Left _ -> Lua.peekList topOfStack `finally` Lua.pop 1
@@ -234,11 +235,16 @@ singleElement x = do
234235 if elementUnchanged
235236 then x <$ Lua.pop 1
236237 else do
237− mbres <- Lua.peekEither (-1)
238+ mbres <- peekEither (-1)
238239 case mbres of
239240 Right res -> res <$ Lua.pop 1
240241 Left err -> do
241242 Lua.pop 1
242− Lua.throwException $
243− "Error while trying to get a filter's return " ++
244− "value from lua stack.\n" ++ err
243+ Lua.throwMessage
244+ ("Error while trying to get a filter's return " <>
245+ "value from Lua stack.\n" <> show err)
246+
247+-- | Try to convert the value at the given stack index to a Haskell value.
248+-- Returns @Left@ with an error message on failure.
249+peekEither :: Peekable a => StackIndex -> Lua (Either PandocError a)
250+peekEither = try . Lua.peek
src/Text/Pandoc/Lua/Init.hs+26−65
@@ -9,96 +9,57 @@
99Functions to initialize the Lua interpreter.
1010-}
1111module Text.Pandoc.Lua.Init
12− ( LuaException (..)
13− , LuaPackageParams (..)
14− , runLua
15− , luaPackageParams
12+ ( runLua
1613 ) where
1714
15+import Control.Monad.Catch (try)
1816import Control.Monad.Trans (MonadIO (..))
1917import Data.Data (Data, dataTypeConstrs, dataTypeOf, showConstr)
2018import Foreign.Lua (Lua)
2119import GHC.IO.Encoding (getForeignEncoding, setForeignEncoding, utf8)
2220import Text.Pandoc.Class.PandocIO (PandocIO)
23−import Text.Pandoc.Class.PandocMonad (getCommonState, getUserDataDir,
24− putCommonState)
25−import Text.Pandoc.Lua.Global (Global (..), setGlobals)
26−import Text.Pandoc.Lua.Packages (LuaPackageParams (..),
27− installPandocPackageSearcher)
28−import Text.Pandoc.Lua.Util (loadScriptFromDataDir)
21+import Text.Pandoc.Error (PandocError)
22+import Text.Pandoc.Lua.Packages (installPandocPackageSearcher)
23+import Text.Pandoc.Lua.PandocLua (PandocLua, liftPandocLua,
24+ loadScriptFromDataDir, runPandocLua)
2925
30−import qualified Data.Text as Text
3126import qualified Foreign.Lua as Lua
32−import qualified Foreign.Lua.Module.Text as Lua
3327import qualified Text.Pandoc.Definition as Pandoc
3428import qualified Text.Pandoc.Lua.Module.Pandoc as ModulePandoc
3529
36−-- | Lua error message
37−newtype LuaException = LuaException Text.Text deriving (Show)
38−
3930-- | Run the lua interpreter, using pandoc's default way of environment
4031-- initialization.
41−runLua :: Lua a -> PandocIO (Either LuaException a)
32+runLua :: Lua a -> PandocIO (Either PandocError a)
4233runLua luaOp = do
43− luaPkgParams <- luaPackageParams
44− globals <- defaultGlobals
4534 enc <- liftIO $ getForeignEncoding <* setForeignEncoding utf8
46− res <- liftIO . Lua.runEither $ do
47− setGlobals globals
48− initLuaState luaPkgParams
49− -- run the given Lua operation
50− opResult <- luaOp
51− -- get the (possibly modified) state back
52− Lua.getglobal "PANDOC_STATE"
53− st <- Lua.peek Lua.stackTop
54− Lua.pop 1
55− -- done
56− return (opResult, st)
35+ res <- runPandocLua . try $ do
36+ initLuaState
37+ liftPandocLua luaOp
5738 liftIO $ setForeignEncoding enc
58− case res of
59− Left (Lua.Exception msg) -> return $ Left (LuaException $ Text.pack msg)
60− Right (x, newState) -> do
61− putCommonState newState
62− return $ Right x
63−
64−-- | Global variables which should always be set.
65−defaultGlobals :: PandocIO [Global]
66−defaultGlobals = do
67− commonState <- getCommonState
68− return
69− [ PANDOC_API_VERSION
70− , PANDOC_STATE commonState
71− , PANDOC_VERSION
72− ]
73−
74−-- | Generate parameters required to setup pandoc's lua environment.
75−luaPackageParams :: PandocIO LuaPackageParams
76−luaPackageParams = do
77− datadir <- getUserDataDir
78− return LuaPackageParams { luaPkgDataDir = datadir }
39+ return res
7940
8041-- | Initialize the lua state with all required values
81−initLuaState :: LuaPackageParams -> Lua ()
82−initLuaState pkgParams = do
83− Lua.openlibs
84− Lua.preloadTextModule "text"
85− installPandocPackageSearcher pkgParams
42+initLuaState :: PandocLua ()
43+initLuaState = do
44+ liftPandocLua Lua.openlibs
45+ installPandocPackageSearcher
8646 initPandocModule
87− loadScriptFromDataDir (luaPkgDataDir pkgParams) "init.lua"
47+ loadScriptFromDataDir "init.lua"
8848 where
89− initPandocModule :: Lua ()
49+ initPandocModule :: PandocLua ()
9050 initPandocModule = do
9151 -- Push module table
92− ModulePandoc.pushModule (luaPkgDataDir pkgParams)
52+ ModulePandoc.pushModule
9353 -- register as loaded module
94− Lua.pushvalue Lua.stackTop
95− Lua.getfield Lua.registryindex Lua.loadedTableRegistryField
96− Lua.setfield (Lua.nthFromTop 2) "pandoc"
97− Lua.pop 1
54+ liftPandocLua $ do
55+ Lua.pushvalue Lua.stackTop
56+ Lua.getfield Lua.registryindex Lua.loadedTableRegistryField
57+ Lua.setfield (Lua.nthFromTop 2) "pandoc"
58+ Lua.pop 1
9859 -- copy constructors into registry
9960 putConstructorsInRegistry
10061 -- assign module to global variable
101− Lua.setglobal "pandoc"
62+ liftPandocLua $ Lua.setglobal "pandoc"
10263
10364-- | AST elements are marshaled via normal constructor functions in the
10465-- @pandoc@ module. However, accessing Lua globals from Haskell is
@@ -108,8 +69,8 @@ initLuaState pkgParams = do
10869--
10970-- This function expects the @pandoc@ module to be at the top of the
11071-- stack.
111−putConstructorsInRegistry :: Lua ()
112−putConstructorsInRegistry = do
72+putConstructorsInRegistry :: PandocLua ()
73+putConstructorsInRegistry = liftPandocLua $ do
11374 constrsToReg $ Pandoc.Pandoc mempty mempty
11475 constrsToReg $ Pandoc.Str mempty
11576 constrsToReg $ Pandoc.Para mempty
src/Text/Pandoc/Lua/Marshaling.hs+2−1
@@ -13,6 +13,7 @@ module Text.Pandoc.Lua.Marshaling () where
1313
1414import Text.Pandoc.Lua.Marshaling.AST ()
1515import Text.Pandoc.Lua.Marshaling.CommonState ()
16−import Text.Pandoc.Lua.Marshaling.ReaderOptions ()
1716import Text.Pandoc.Lua.Marshaling.Context ()
17+import Text.Pandoc.Lua.Marshaling.PandocError()
18+import Text.Pandoc.Lua.Marshaling.ReaderOptions ()
1819import Text.Pandoc.Lua.Marshaling.Version ()
src/Text/Pandoc/Lua/Marshaling/AST.hs+10−5
@@ -19,9 +19,11 @@ module Text.Pandoc.Lua.Marshaling.AST
1919import Control.Applicative ((<|>))
2020import Foreign.Lua (Lua, Peekable, Pushable, StackIndex)
2121import Text.Pandoc.Definition
22+import Text.Pandoc.Error (PandocError)
2223import Text.Pandoc.Lua.Util (defineHowTo, pushViaConstructor)
2324import Text.Pandoc.Lua.Marshaling.CommonState ()
2425
26+import qualified Control.Monad.Catch as Catch
2527import qualified Foreign.Lua as Lua
2628import qualified Text.Pandoc.Lua.Util as LuaUtil
2729
@@ -131,7 +133,7 @@ peekMetaValue idx = defineHowTo "get MetaValue" $ do
131133 Lua.TypeBoolean -> MetaBool <$> Lua.peek idx
132134 Lua.TypeString -> MetaString <$> Lua.peek idx
133135 Lua.TypeTable -> do
134− tag <- Lua.try $ LuaUtil.getTag idx
136+ tag <- try $ LuaUtil.getTag idx
135137 case tag of
136138 Right "MetaBlocks" -> MetaBlocks <$> elementContent
137139 Right "MetaBool" -> MetaBool <$> elementContent
@@ -139,7 +141,7 @@ peekMetaValue idx = defineHowTo "get MetaValue" $ do
139141 Right "MetaInlines" -> MetaInlines <$> elementContent
140142 Right "MetaList" -> MetaList <$> elementContent
141143 Right "MetaString" -> MetaString <$> elementContent
142− Right t -> Lua.throwException ("Unknown meta tag: " <> t)
144+ Right t -> Lua.throwMessage ("Unknown meta tag: " <> t)
143145 Left _ -> do
144146 -- no meta value tag given, try to guess.
145147 len <- Lua.rawlen idx
@@ -148,7 +150,7 @@ peekMetaValue idx = defineHowTo "get MetaValue" $ do
148150 else (MetaInlines <$> Lua.peek idx)
149151 <|> (MetaBlocks <$> Lua.peek idx)
150152 <|> (MetaList <$> Lua.peek idx)
151− _ -> Lua.throwException "could not get meta value"
153+ _ -> Lua.throwMessage "could not get meta value"
152154
153155-- | Push a block element to the top of the Lua stack.
154156pushBlock :: Block -> Lua ()
@@ -199,7 +201,7 @@ peekBlock idx = defineHowTo "get Block value" $ do
199201 tbodies
200202 tfoot)
201203 <$> elementContent
202− _ -> Lua.throwException ("Unknown block type: " <> tag)
204+ _ -> Lua.throwMessage ("Unknown block type: " <> tag)
203205 where
204206 -- Get the contents of an AST element.
205207 elementContent :: Peekable a => Lua a
@@ -344,12 +346,15 @@ peekInline idx = defineHowTo "get Inline value" $ do
344346 "Strong" -> Strong <$> elementContent
345347 "Subscript" -> Subscript <$> elementContent
346348 "Superscript"-> Superscript <$> elementContent
347− _ -> Lua.throwException ("Unknown inline type: " <> tag)
349+ _ -> Lua.throwMessage ("Unknown inline type: " <> tag)
348350 where
349351 -- Get the contents of an AST element.
350352 elementContent :: Peekable a => Lua a
351353 elementContent = LuaUtil.rawField idx "c"
352354
355+try :: Lua a -> Lua (Either PandocError a)
356+try = Catch.try
357+
353358withAttr :: (Attr -> a -> b) -> (LuaAttr, a) -> b
354359withAttr f (attributes, x) = f (fromLuaAttr attributes) x
355360
src/Text/Pandoc/Lua/Marshaling/PandocError.hsnew file+65
@@ -0,0 +1,65 @@
1+{-# OPTIONS_GHC -fno-warn-orphans #-}
2+{-# LANGUAGE LambdaCase #-}
3+{-# LANGUAGE OverloadedStrings #-}
4+{-# LANGUAGE ScopedTypeVariables #-}
5+{- |
6+ Module : Text.Pandoc.Lua.Marshaling.PandocError
7+ Copyright : © 2020 Albert Krewinkel
8+ License : GNU GPL, version 2 or above
9+
10+ Maintainer : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
11+ Stability : alpha
12+
13+Marshaling of @'PandocError'@ values.
14+-}
15+module Text.Pandoc.Lua.Marshaling.PandocError
16+ ( peekPandocError
17+ , pushPandocError
18+ )
19+ where
20+
21+import Foreign.Lua (Lua, Peekable, Pushable, StackIndex)
22+import Text.Pandoc.Error (PandocError (PandocLuaError))
23+
24+import qualified Foreign.Lua as Lua
25+import qualified Foreign.Lua.Userdata as Lua
26+import qualified Text.Pandoc.Lua.Util as LuaUtil
27+import qualified Text.Pandoc.UTF8 as UTF8
28+
29+-- | Userdata name used by Lua for the @PandocError@ type.
30+pandocErrorName :: String
31+pandocErrorName = "pandoc error"
32+
33+-- | Peek a @'PandocError'@ element to the Lua stack.
34+pushPandocError :: PandocError -> Lua ()
35+pushPandocError = Lua.pushAnyWithMetatable pushPandocErrorMT
36+ where
37+ pushPandocErrorMT = Lua.ensureUserdataMetatable pandocErrorName $
38+ LuaUtil.addFunction "__tostring" __tostring
39+
40+-- | Retrieve a @'PandocError'@ from the Lua stack.
41+peekPandocError :: StackIndex -> Lua PandocError
42+peekPandocError idx = Lua.ltype idx >>= \case
43+ Lua.TypeUserdata -> do
44+ errMb <- Lua.toAnyWithName idx pandocErrorName
45+ return $ case errMb of
46+ Just err -> err
47+ Nothing -> PandocLuaError "could not retrieve original error"
48+ _ -> do
49+ Lua.pushvalue idx
50+ msg <- Lua.state >>= \l -> Lua.liftIO (Lua.errorMessage l)
51+ return $ PandocLuaError (UTF8.toText msg)
52+
53+-- | Convert to string.
54+__tostring :: PandocError -> Lua String
55+__tostring = return . show
56+
57+--
58+-- Instances
59+--
60+
61+instance Pushable PandocError where
62+ push = pushPandocError
63+
64+instance Peekable PandocError where
65+ peek = peekPandocError
src/Text/Pandoc/Lua/Marshaling/Version.hs+2−2
@@ -57,7 +57,7 @@ peekVersion idx = Lua.ltype idx >>= \case
5757 let parses = readP_to_S parseVersion versionStr
5858 case lastMay parses of
5959 Just (v, "") -> return v
60− _ -> Lua.throwException $ "could not parse as Version: " ++ versionStr
60+ _ -> Lua.throwMessage $ "could not parse as Version: " ++ versionStr
6161
6262 Lua.TypeUserdata ->
6363 reportValueOnFailure versionTypeName
@@ -71,7 +71,7 @@ peekVersion idx = Lua.ltype idx >>= \case
7171 makeVersion <$> Lua.peek idx
7272
7373 _ ->
74− Lua.throwException "could not peek Version"
74+ Lua.throwMessage "could not peek Version"
7575
7676instance Peekable Version where
7777 peek = peekVersion
src/Text/Pandoc/Lua/Module/MediaBag.hs+26−51
@@ -14,13 +14,13 @@ module Text.Pandoc.Lua.Module.MediaBag
1414 ) where
1515
1616import Control.Monad (zipWithM_)
17−import Foreign.Lua (Lua, NumResults, Optional, liftIO)
17+import Foreign.Lua (Lua, NumResults, Optional)
1818import Text.Pandoc.Class.CommonState (CommonState (..))
19−import Text.Pandoc.Class.PandocIO (runIOorExplode)
20−import Text.Pandoc.Class.PandocMonad (fetchItem, putCommonState, setMediaBag)
19+import Text.Pandoc.Class.PandocMonad (fetchItem, getMediaBag, modifyCommonState,
20+ setMediaBag)
2121import Text.Pandoc.Lua.Marshaling ()
2222import Text.Pandoc.Lua.Marshaling.MediaBag (pushIterator)
23−import Text.Pandoc.Lua.Util (addFunction)
23+import Text.Pandoc.Lua.PandocLua (PandocLua (..), liftPandocLua, addFunction)
2424import Text.Pandoc.MIME (MimeType)
2525
2626import qualified Data.ByteString.Lazy as BL
@@ -31,9 +31,9 @@ import qualified Text.Pandoc.MediaBag as MB
3131--
3232-- MediaBag submodule
3333--
34−pushModule :: Lua NumResults
34+pushModule :: PandocLua NumResults
3535pushModule = do
36− Lua.newtable
36+ liftPandocLua Lua.newtable
3737 addFunction "delete" delete
3838 addFunction "empty" empty
3939 addFunction "insert" insertMediaFn
@@ -43,66 +43,46 @@ pushModule = do
4343 addFunction "fetch" fetch
4444 return 1
4545
46−--
47−-- Port functions from Text.Pandoc.Class to the Lua monad.
48−-- TODO: reuse existing functions.
49−
50−-- Get the current CommonState.
51−getCommonState :: Lua CommonState
52−getCommonState = do
53− Lua.getglobal "PANDOC_STATE"
54− Lua.peek Lua.stackTop
55−
56−-- Replace MediaBag in CommonState.
57−setCommonState :: CommonState -> Lua ()
58−setCommonState st = do
59− Lua.push st
60− Lua.setglobal "PANDOC_STATE"
61−
62−modifyCommonState :: (CommonState -> CommonState) -> Lua ()
63−modifyCommonState f = getCommonState >>= setCommonState . f
64−
6546-- | Delete a single item from the media bag.
66−delete :: FilePath -> Lua NumResults
47+delete :: FilePath -> PandocLua NumResults
6748delete fp = 0 <$ modifyCommonState
6849 (\st -> st { stMediaBag = MB.deleteMedia fp (stMediaBag st) })
6950
7051-- | Delete all items from the media bag.
71−empty :: Lua NumResults
52+empty :: PandocLua NumResults
7253empty = 0 <$ modifyCommonState (\st -> st { stMediaBag = mempty })
7354
7455-- | Insert a new item into the media bag.
7556insertMediaFn :: FilePath
7657 -> Optional MimeType
7758 -> BL.ByteString
78− -> Lua NumResults
59+ -> PandocLua NumResults
7960insertMediaFn fp optionalMime contents = do
80− modifyCommonState $ \st ->
81− let mb = MB.insertMedia fp (Lua.fromOptional optionalMime) contents
82− (stMediaBag st)
83− in st { stMediaBag = mb }
84− return 0
61+ mb <- getMediaBag
62+ setMediaBag $ MB.insertMedia fp (Lua.fromOptional optionalMime) contents mb
63+ return (Lua.NumResults 0)
8564
8665-- | Returns iterator values to be used with a Lua @for@ loop.
87−items :: Lua NumResults
88−items = getCommonState >>= pushIterator . stMediaBag
66+items :: PandocLua NumResults
67+items = getMediaBag >>= liftPandocLua . pushIterator
8968
9069lookupMediaFn :: FilePath
91− -> Lua NumResults
70+ -> PandocLua NumResults
9271lookupMediaFn fp = do
93− res <- MB.lookupMedia fp . stMediaBag <$> getCommonState
94− case res of
72+ res <- MB.lookupMedia fp <$> getMediaBag
73+ liftPandocLua $ case res of
9574 Nothing -> 1 <$ Lua.pushnil
9675 Just (mimeType, contents) -> do
9776 Lua.push mimeType
9877 Lua.push contents
9978 return 2
10079
101−mediaDirectoryFn :: Lua NumResults
80+mediaDirectoryFn :: PandocLua NumResults
10281mediaDirectoryFn = do
103− dirContents <- MB.mediaDirectory . stMediaBag <$> getCommonState
104− Lua.newtable
105− zipWithM_ addEntry [1..] dirContents
82+ dirContents <- MB.mediaDirectory <$> getMediaBag
83+ liftPandocLua $ do
84+ Lua.newtable
85+ zipWithM_ addEntry [1..] dirContents
10686 return 1
10787 where
10888 addEntry :: Lua.Integer -> (FilePath, MimeType, Int) -> Lua ()
@@ -114,14 +94,9 @@ mediaDirectoryFn = do
11494 Lua.rawseti (-2) idx
11595
11696fetch :: T.Text
117− -> Lua NumResults
97+ -> PandocLua NumResults
11898fetch src = do
119− commonState <- getCommonState
120− let mediaBag = stMediaBag commonState
121− (bs, mimeType) <- liftIO . runIOorExplode $ do
122− putCommonState commonState
123− setMediaBag mediaBag
124− fetchItem src
125− Lua.push $ maybe "" T.unpack mimeType
126− Lua.push bs
99+ (bs, mimeType) <- fetchItem src
100+ liftPandocLua . Lua.push $ maybe "" T.unpack mimeType
101+ liftPandocLua $ Lua.push bs
127102 return 2 -- returns 2 values: contents, mimetype
src/Text/Pandoc/Lua/Module/Pandoc.hs+17−15
@@ -24,6 +24,8 @@ import Text.Pandoc.Class.PandocIO (runIO)
2424import Text.Pandoc.Definition (Block, Inline)
2525import Text.Pandoc.Lua.Filter (walkInlines, walkBlocks, LuaFilter, SingletonsList (..))
2626import Text.Pandoc.Lua.Marshaling ()
27+import Text.Pandoc.Lua.PandocLua (PandocLua, addFunction, liftPandocLua,
28+ loadScriptFromDataDir)
2729import Text.Pandoc.Walk (Walkable)
2830import Text.Pandoc.Options (ReaderOptions (readerExtensions))
2931import Text.Pandoc.Process (pipeProcess)
@@ -38,28 +40,28 @@ import Text.Pandoc.Error
3840
3941-- | Push the "pandoc" on the lua stack. Requires the `list` module to be
4042-- loaded.
41−pushModule :: Maybe FilePath -> Lua NumResults
42−pushModule datadir = do
43− LuaUtil.loadScriptFromDataDir datadir "pandoc.lua"
44− LuaUtil.addFunction "read" readDoc
45− LuaUtil.addFunction "pipe" pipeFn
46− LuaUtil.addFunction "walk_block" walkBlock
47− LuaUtil.addFunction "walk_inline" walkInline
43+pushModule :: PandocLua NumResults
44+pushModule = do
45+ loadScriptFromDataDir "pandoc.lua"
46+ addFunction "read" readDoc
47+ addFunction "pipe" pipeFn
48+ addFunction "walk_block" walkBlock
49+ addFunction "walk_inline" walkInline
4850 return 1
4951
5052walkElement :: (Walkable (SingletonsList Inline) a,
5153 Walkable (SingletonsList Block) a)
52− => a -> LuaFilter -> Lua a
53−walkElement x f = walkInlines f x >>= walkBlocks f
54+ => a -> LuaFilter -> PandocLua a
55+walkElement x f = liftPandocLua $ walkInlines f x >>= walkBlocks f
5456
55−walkInline :: Inline -> LuaFilter -> Lua Inline
57+walkInline :: Inline -> LuaFilter -> PandocLua Inline
5658walkInline = walkElement
5759
58−walkBlock :: Block -> LuaFilter -> Lua Block
60+walkBlock :: Block -> LuaFilter -> PandocLua Block
5961walkBlock = walkElement
6062
61−readDoc :: T.Text -> Optional T.Text -> Lua NumResults
62−readDoc content formatSpecOrNil = do
63+readDoc :: T.Text -> Optional T.Text -> PandocLua NumResults
64+readDoc content formatSpecOrNil = liftPandocLua $ do
6365 let formatSpec = fromMaybe "markdown" (Lua.fromOptional formatSpecOrNil)
6466 res <- Lua.liftIO . runIO $
6567 getReader formatSpec >>= \(rdr,es) ->
@@ -80,8 +82,8 @@ readDoc content formatSpecOrNil = do
8082pipeFn :: String
8183 -> [String]
8284 -> BL.ByteString
83− -> Lua NumResults
84−pipeFn command args input = do
85+ -> PandocLua NumResults
86+pipeFn command args input = liftPandocLua $ do
8587 (ec, output) <- Lua.liftIO $ pipeProcess Nothing command args input
8688 case ec of
8789 ExitSuccess -> 1 <$ Lua.push output
src/Text/Pandoc/Lua/Module/Utils.hs+26−31
@@ -1,3 +1,4 @@
1+{-# LANGUAGE ScopedTypeVariables #-}
12{- |
23 Module : Text.Pandoc.Lua.Module.Utils
34 Copyright : Copyright © 2017-2020 Albert Krewinkel
@@ -13,15 +14,15 @@ module Text.Pandoc.Lua.Module.Utils
1314 ) where
1415
1516import Control.Applicative ((<|>))
17+import Control.Monad.Catch (try)
1618import Data.Default (def)
1719import Data.Version (Version)
1820import Foreign.Lua (Peekable, Lua, NumResults)
19−import Text.Pandoc.Class.PandocIO (runIO)
20−import Text.Pandoc.Class.PandocMonad (setUserDataDir)
2121import Text.Pandoc.Definition ( Pandoc, Meta, MetaValue (..), Block, Inline
2222 , Citation, Attr, ListAttributes)
23+import Text.Pandoc.Error (PandocError)
2324import Text.Pandoc.Lua.Marshaling ()
24−import Text.Pandoc.Lua.Util (addFunction)
25+import Text.Pandoc.Lua.PandocLua (PandocLua, addFunction, liftPandocLua)
2526
2627import qualified Data.Digest.Pure.SHA as SHA
2728import qualified Data.ByteString.Lazy as BSL
@@ -32,14 +33,14 @@ import qualified Text.Pandoc.Filter.JSON as JSONFilter
3233import qualified Text.Pandoc.Shared as Shared
3334
3435-- | Push the "pandoc.utils" module to the lua stack.
35−pushModule :: Maybe FilePath -> Lua NumResults
36−pushModule mbDatadir = do
37− Lua.newtable
36+pushModule :: PandocLua NumResults
37+pushModule = do
38+ liftPandocLua Lua.newtable
3839 addFunction "blocks_to_inlines" blocksToInlines
3940 addFunction "equals" equals
4041 addFunction "make_sections" makeSections
4142 addFunction "normalize_date" normalizeDate
42− addFunction "run_json_filter" (runJSONFilter mbDatadir)
43+ addFunction "run_json_filter" runJSONFilter
4344 addFunction "sha1" sha1
4445 addFunction "stringify" stringify
4546 addFunction "to_roman_numeral" toRomanNumeral
@@ -47,8 +48,8 @@ pushModule mbDatadir = do
4748 return 1
4849
4950-- | Squashes a list of blocks into inlines.
50−blocksToInlines :: [Block] -> Lua.Optional [Inline] -> Lua [Inline]
51−blocksToInlines blks optSep = do
51+blocksToInlines :: [Block] -> Lua.Optional [Inline] -> PandocLua [Inline]
52+blocksToInlines blks optSep = liftPandocLua $ do
5253 let sep = case Lua.fromOptional optSep of
5354 Just x -> B.fromList x
5455 Nothing -> Shared.defaultBlocksSeparator
@@ -67,23 +68,17 @@ normalizeDate :: T.Text -> Lua (Lua.Optional T.Text)
6768normalizeDate = return . Lua.Optional . Shared.normalizeDate
6869
6970-- | Run a JSON filter on the given document.
70−runJSONFilter :: Maybe FilePath
71− -> Pandoc
71+runJSONFilter :: Pandoc
7272 -> FilePath
7373 -> Lua.Optional [String]
74− -> Lua NumResults
75−runJSONFilter mbDatadir doc filterFile optArgs = do
74+ -> PandocLua Pandoc
75+runJSONFilter doc filterFile optArgs = do
7676 args <- case Lua.fromOptional optArgs of
7777 Just x -> return x
78− Nothing -> do
78+ Nothing -> liftPandocLua $ do
7979 Lua.getglobal "FORMAT"
8080 (:[]) <$> Lua.popValue
81− filterRes <- Lua.liftIO . runIO $ do
82− setUserDataDir mbDatadir
83− JSONFilter.apply def args filterFile doc
84− case filterRes of
85− Left err -> Lua.raiseError (show err)
86− Right d -> (1 :: NumResults) <$ Lua.push d
81+ JSONFilter.apply def args filterFile doc
8782
8883-- | Calculate the hash of the given contents.
8984sha1 :: BSL.ByteString
@@ -93,7 +88,7 @@ sha1 = return . T.pack . SHA.showDigest . SHA.sha1
9388-- | Convert pandoc structure to a string with formatting removed.
9489-- Footnotes are skipped (since we don't want their contents in link
9590-- labels).
96−stringify :: AstElement -> Lua T.Text
91+stringify :: AstElement -> PandocLua T.Text
9792stringify el = return $ case el of
9893 PandocElement pd -> Shared.stringify pd
9994 InlineElement i -> Shared.stringify i
@@ -109,7 +104,7 @@ stringifyMetaValue mv = case mv of
109104 MetaString s -> s
110105 _ -> Shared.stringify mv
111106
112−equals :: AstElement -> AstElement -> Lua Bool
107+equals :: AstElement -> AstElement -> PandocLua Bool
113108equals e1 e2 = return (e1 == e2)
114109
115110data AstElement
@@ -125,18 +120,18 @@ data AstElement
125120
126121instance Peekable AstElement where
127122 peek idx = do
128− res <- Lua.try $ (PandocElement <$> Lua.peek idx)
129− <|> (InlineElement <$> Lua.peek idx)
130− <|> (BlockElement <$> Lua.peek idx)
131− <|> (AttrElement <$> Lua.peek idx)
132− <|> (ListAttributesElement <$> Lua.peek idx)
133− <|> (MetaElement <$> Lua.peek idx)
134− <|> (MetaValueElement <$> Lua.peek idx)
123+ res <- try $ (PandocElement <$> Lua.peek idx)
124+ <|> (InlineElement <$> Lua.peek idx)
125+ <|> (BlockElement <$> Lua.peek idx)
126+ <|> (AttrElement <$> Lua.peek idx)
127+ <|> (ListAttributesElement <$> Lua.peek idx)
128+ <|> (MetaElement <$> Lua.peek idx)
129+ <|> (MetaValueElement <$> Lua.peek idx)
135130 case res of
136131 Right x -> return x
137− Left _ -> Lua.throwException
132+ Left (_ :: PandocError) -> Lua.throwMessage
138133 "Expected an AST element, but could not parse value as such."
139134
140135-- | Convert a number < 4000 to uppercase roman numeral.
141−toRomanNumeral :: Lua.Integer -> Lua T.Text
136+toRomanNumeral :: Lua.Integer -> PandocLua T.Text
142137toRomanNumeral = return . Shared.toRomanNumeral . fromIntegral
src/Text/Pandoc/Lua/Packages.hs+23−41
@@ -8,37 +8,32 @@
88 Maintainer : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
99 Stability : alpha
1010
11−Pandoc module for lua.
11+Pandoc module for Lua.
1212-}
1313module Text.Pandoc.Lua.Packages
14− ( LuaPackageParams (..)
15− , installPandocPackageSearcher
14+ ( installPandocPackageSearcher
1615 ) where
1716
1817import Control.Monad (forM_)
1918import Data.ByteString (ByteString)
20−import Foreign.Lua (Lua, NumResults, liftIO)
21−import Text.Pandoc.Class.PandocIO (runIO)
22−import Text.Pandoc.Class.PandocMonad (readDataFile, setUserDataDir)
19+import Foreign.Lua (Lua, NumResults)
20+import Text.Pandoc.Class.PandocMonad (readDataFile)
21+import Text.Pandoc.Lua.PandocLua (PandocLua, liftPandocLua)
2322
2423import qualified Foreign.Lua as Lua
25−import Text.Pandoc.Lua.Module.Pandoc as Pandoc
26−import Text.Pandoc.Lua.Module.MediaBag as MediaBag
27−import Text.Pandoc.Lua.Module.System as System
28−import Text.Pandoc.Lua.Module.Types as Types
29−import Text.Pandoc.Lua.Module.Utils as Utils
30−
31−-- | Parameters used to create lua packages/modules.
32−data LuaPackageParams = LuaPackageParams
33− { luaPkgDataDir :: Maybe FilePath
34− }
24+import qualified Foreign.Lua.Module.Text as Text
25+import qualified Text.Pandoc.Lua.Module.Pandoc as Pandoc
26+import qualified Text.Pandoc.Lua.Module.MediaBag as MediaBag
27+import qualified Text.Pandoc.Lua.Module.System as System
28+import qualified Text.Pandoc.Lua.Module.Types as Types
29+import qualified Text.Pandoc.Lua.Module.Utils as Utils
3530
3631-- | Insert pandoc's package loader as the first loader, making it the default.
37−installPandocPackageSearcher :: LuaPackageParams -> Lua ()
38−installPandocPackageSearcher luaPkgParams = do
32+installPandocPackageSearcher :: PandocLua ()
33+installPandocPackageSearcher = liftPandocLua $ do
3934 Lua.getglobal' "package.searchers"
4035 shiftArray
41− Lua.pushHaskellFunction (pandocPackageSearcher luaPkgParams)
36+ Lua.pushHaskellFunction pandocPackageSearcher
4237 Lua.rawseti (Lua.nthFromTop 2) 1
4338 Lua.pop 1 -- remove 'package.searchers' from stack
4439 where
@@ -47,29 +42,24 @@ installPandocPackageSearcher luaPkgParams = do
4742 Lua.rawseti (-2) (i + 1)
4843
4944-- | Load a pandoc module.
50−pandocPackageSearcher :: LuaPackageParams -> String -> Lua NumResults
51−pandocPackageSearcher pkgParams pkgName =
45+pandocPackageSearcher :: String -> PandocLua NumResults
46+pandocPackageSearcher pkgName =
5247 case pkgName of
53− "pandoc" -> let datadir = luaPkgDataDir pkgParams
54− in pushWrappedHsFun (Pandoc.pushModule datadir)
48+ "pandoc" -> pushWrappedHsFun Pandoc.pushModule
5549 "pandoc.mediabag" -> pushWrappedHsFun MediaBag.pushModule
5650 "pandoc.system" -> pushWrappedHsFun System.pushModule
5751 "pandoc.types" -> pushWrappedHsFun Types.pushModule
58− "pandoc.utils" -> let datadir = luaPkgDataDir pkgParams
59− in pushWrappedHsFun (Utils.pushModule datadir)
60− _ -> searchPureLuaLoader
52+ "pandoc.utils" -> pushWrappedHsFun Utils.pushModule
53+ "text" -> pushWrappedHsFun Text.pushModule
54+ _ -> searchPureLuaLoader
6155 where
62− pushWrappedHsFun f = do
56+ pushWrappedHsFun f = liftPandocLua $ do
6357 Lua.pushHaskellFunction f
6458 return 1
6559 searchPureLuaLoader = do
6660 let filename = pkgName ++ ".lua"
67− modScript <- liftIO (dataDirScript (luaPkgDataDir pkgParams) filename)
68− case modScript of
69− Just script -> pushWrappedHsFun (loadStringAsPackage pkgName script)
70− Nothing -> do
71− Lua.push ("\n\tno file '" ++ filename ++ "' in pandoc's datadir")
72− return 1
61+ script <- readDataFile filename
62+ pushWrappedHsFun (loadStringAsPackage pkgName script)
7363
7464loadStringAsPackage :: String -> ByteString -> Lua NumResults
7565loadStringAsPackage pkgName script = do
@@ -79,11 +69,3 @@ loadStringAsPackage pkgName script = do
7969 else do
8070 msg <- Lua.popValue
8171 Lua.raiseError ("Error while loading `" <> pkgName <> "`.\n" <> msg)
82−
83−-- | Get the ByteString representation of the pandoc module.
84−dataDirScript :: Maybe FilePath -> FilePath -> IO (Maybe ByteString)
85−dataDirScript datadir moduleFile = do
86− res <- runIO $ setUserDataDir datadir >> readDataFile moduleFile
87− return $ case res of
88− Left _ -> Nothing
89− Right s -> Just s
src/Text/Pandoc/Lua/PandocLua.hsnew file+134
@@ -0,0 +1,134 @@
1+{-# LANGUAGE DeriveFunctor #-}
2+{-# LANGUAGE FlexibleContexts #-}
3+{-# LANGUAGE FlexibleInstances #-}
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
5+{-# LANGUAGE MultiParamTypeClasses #-}
6+{-# LANGUAGE OverloadedStrings #-}
7+{-# LANGUAGE RankNTypes #-}
8+{-# LANGUAGE ScopedTypeVariables #-}
9+{-# OPTIONS_GHC -fno-warn-orphans #-}
10+{- |
11+ Module : Text.Pandoc.Lua.PandocLua
12+ Copyright : Copyright © 2020 Albert Krewinkel
13+ License : GNU GPL, version 2 or above
14+
15+ Maintainer : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
16+ Stability : alpha
17+
18+PandocMonad instance which allows execution of Lua operations and which
19+uses Lua to handle state.
20+-}
21+module Text.Pandoc.Lua.PandocLua
22+ ( PandocLua (..)
23+ , runPandocLua
24+ , liftPandocLua
25+ , addFunction
26+ , loadScriptFromDataDir
27+ ) where
28+
29+import Control.Monad (when)
30+import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
31+import Control.Monad.Except (MonadError (catchError, throwError))
32+import Control.Monad.IO.Class (MonadIO (liftIO))
33+import Foreign.Lua (Lua (..), NumResults, Pushable, ToHaskellFunction)
34+import Text.Pandoc.Class.PandocIO (PandocIO)
35+import Text.Pandoc.Class.PandocMonad (PandocMonad (..), readDataFile)
36+import Text.Pandoc.Error (PandocError)
37+import Text.Pandoc.Lua.Global (Global (..), setGlobals)
38+import Text.Pandoc.Lua.ErrorConversion (errorConversion)
39+
40+import qualified Control.Monad.Catch as Catch
41+import qualified Foreign.Lua as Lua
42+import qualified Text.Pandoc.Class.IO as IO
43+import qualified Text.Pandoc.Lua.Util as LuaUtil
44+
45+-- | Type providing access to both, pandoc and Lua operations.
46+newtype PandocLua a = PandocLua { unPandocLua :: Lua a }
47+ deriving
48+ ( Applicative
49+ , Functor
50+ , Monad
51+ , MonadCatch
52+ , MonadIO
53+ , MonadMask
54+ , MonadThrow
55+ )
56+
57+-- | Lift a @'Lua'@ operation into the @'PandocLua'@ type.
58+liftPandocLua :: Lua a -> PandocLua a
59+liftPandocLua = PandocLua
60+
61+-- | Evaluate a @'PandocLua'@ computation, running all contained Lua
62+-- operations..
63+runPandocLua :: PandocLua a -> PandocIO a
64+runPandocLua pLua = do
65+ origState <- getCommonState
66+ globals <- defaultGlobals
67+ (result, newState) <- liftIO . Lua.run' errorConversion . unPandocLua $ do
68+ putCommonState origState
69+ liftPandocLua $ setGlobals globals
70+ r <- pLua
71+ c <- getCommonState
72+ return (r, c)
73+ putCommonState newState
74+ return result
75+
76+instance {-# OVERLAPPING #-} ToHaskellFunction (PandocLua NumResults) where
77+ toHsFun _narg = unPandocLua
78+
79+instance Pushable a => ToHaskellFunction (PandocLua a) where
80+ toHsFun _narg x = 1 <$ (unPandocLua x >>= Lua.push)
81+
82+-- | Add a function to the table at the top of the stack, using the given name.
83+addFunction :: ToHaskellFunction a => String -> a -> PandocLua ()
84+addFunction name fn = liftPandocLua $ do
85+ Lua.push name
86+ Lua.pushHaskellFunction fn
87+ Lua.rawset (-3)
88+
89+-- | Load a file from pandoc's data directory.
90+loadScriptFromDataDir :: FilePath -> PandocLua ()
91+loadScriptFromDataDir scriptFile = do
92+ script <- readDataFile scriptFile
93+ status <- liftPandocLua $ Lua.dostring script
94+ when (status /= Lua.OK) . liftPandocLua $
95+ LuaUtil.throwTopMessageAsError'
96+ (("Couldn't load '" ++ scriptFile ++ "'.\n") ++)
97+
98+-- | Global variables which should always be set.
99+defaultGlobals :: PandocIO [Global]
100+defaultGlobals = do
101+ commonState <- getCommonState
102+ return
103+ [ PANDOC_API_VERSION
104+ , PANDOC_STATE commonState
105+ , PANDOC_VERSION
106+ ]
107+
108+instance MonadError PandocError PandocLua where
109+ catchError = Catch.catch
110+ throwError = Catch.throwM
111+
112+instance PandocMonad PandocLua where
113+ lookupEnv = IO.lookupEnv
114+ getCurrentTime = IO.getCurrentTime
115+ getCurrentTimeZone = IO.getCurrentTimeZone
116+ newStdGen = IO.newStdGen
117+ newUniqueHash = IO.newUniqueHash
118+
119+ openURL = IO.openURL
120+
121+ readFileLazy = IO.readFileLazy
122+ readFileStrict = IO.readFileStrict
123+
124+ glob = IO.glob
125+ fileExists = IO.fileExists
126+ getDataFileName = IO.getDataFileName
127+ getModificationTime = IO.getModificationTime
128+
129+ getCommonState = PandocLua $ do
130+ Lua.getglobal "PANDOC_STATE"
131+ Lua.peek Lua.stackTop
132+ putCommonState = PandocLua . setGlobals . (:[]) . PANDOC_STATE
133+
134+ logOutput = IO.logOutput
src/Text/Pandoc/Lua/Util.hs+7−17
@@ -19,7 +19,6 @@ module Text.Pandoc.Lua.Util
1919 , addFunction
2020 , addValue
2121 , pushViaConstructor
22− , loadScriptFromDataDir
2322 , defineHowTo
2423 , throwTopMessageAsError'
2524 , callWithTraceback
@@ -27,13 +26,11 @@ module Text.Pandoc.Lua.Util
2726 ) where
2827
2928import Control.Monad (unless, when)
29+import Data.Text (Text)
3030import Foreign.Lua ( Lua, NumArgs, NumResults, Peekable, Pushable, StackIndex
3131 , Status, ToHaskellFunction )
32−import Text.Pandoc.Class.PandocIO (runIOorExplode)
33−import Text.Pandoc.Class.PandocMonad (readDataFile, setUserDataDir)
3432import qualified Foreign.Lua as Lua
3533import qualified Text.Pandoc.UTF8 as UTF8
36−import Data.Text (Text)
3734
3835-- | Get value behind key from table at given index.
3936rawField :: Peekable a => StackIndex -> String -> Lua a
@@ -87,15 +84,6 @@ pushViaCall fn = pushViaCall' fn (return ()) 0
8784pushViaConstructor :: PushViaCall a => String -> a
8885pushViaConstructor pandocFn = pushViaCall ("pandoc." ++ pandocFn)
8986
90−-- | Load a file from pandoc's data directory.
91−loadScriptFromDataDir :: Maybe FilePath -> FilePath -> Lua ()
92−loadScriptFromDataDir datadir scriptFile = do
93− script <- Lua.liftIO . runIOorExplode $
94− setUserDataDir datadir >> readDataFile scriptFile
95− status <- Lua.dostring script
96− when (status /= Lua.OK) $
97− throwTopMessageAsError' (("Couldn't load '" ++ scriptFile ++ "'.\n") ++)
98−
9987-- | Get the tag of a value. This is an optimized and specialized version of
10088-- @Lua.getfield idx "tag"@. It only checks for the field on the table at index
10189-- @idx@ and on its metatable, also ignoring any @__index@ value on the
@@ -107,7 +95,7 @@ getTag idx = do
10795 Lua.push ("tag" :: Text)
10896 Lua.rawget (Lua.nthFromTop 2)
10997 Lua.tostring Lua.stackTop <* Lua.pop 2 >>= \case
110− Nothing -> Lua.throwException "untagged value"
98+ Nothing -> Lua.throwMessage "untagged value"
11199 Just x -> return (UTF8.toString x)
112100
113101-- | Modify the message at the top of the stack before throwing it as an
@@ -116,11 +104,12 @@ throwTopMessageAsError' :: (String -> String) -> Lua a
116104throwTopMessageAsError' modifier = do
117105 msg <- Lua.tostring' Lua.stackTop
118106 Lua.pop 2 -- remove error and error string pushed by tostring'
119− Lua.throwException (modifier (UTF8.toString msg))
107+ Lua.throwMessage (modifier (UTF8.toString msg))
120108
121109-- | Mark the context of a Lua computation for better error reporting.
122110defineHowTo :: String -> Lua a -> Lua a
123−defineHowTo ctx = Lua.withExceptionMessage (("Could not " <> ctx <> ": ") <>)
111+defineHowTo ctx op = Lua.errorConversion >>= \ec ->
112+ Lua.addContextToException ec ("Could not " <> ctx <> ": ") op
124113
125114-- | Like @'Lua.pcall'@, but uses a predefined error handler which adds a
126115-- traceback on error.
@@ -143,7 +132,8 @@ pcallWithTraceback nargs nresults = do
143132callWithTraceback :: NumArgs -> NumResults -> Lua ()
144133callWithTraceback nargs nresults = do
145134 result <- pcallWithTraceback nargs nresults
146− when (result /= Lua.OK) Lua.throwTopMessage
135+ when (result /= Lua.OK)
136+ Lua.throwTopMessage
147137
148138-- | Run the given string as a Lua program, while also adding a traceback to the
149139-- error message if an error occurs.
src/Text/Pandoc/Writers/Custom.hs+9−18
@@ -20,17 +20,14 @@ import Data.List (intersperse)
2020import qualified Data.Map as M
2121import qualified Data.Text as T
2222import Data.Text (Text, pack)
23−import Data.Typeable
2423import Foreign.Lua (Lua, Pushable)
2524import Text.DocLayout (render, literal)
2625import Text.Pandoc.Class.PandocIO (PandocIO)
2726import Text.Pandoc.Definition
28−import Text.Pandoc.Lua (Global (..), LuaException (LuaException),
29− runLua, setGlobals)
27+import Text.Pandoc.Lua (Global (..), runLua, setGlobals)
3028import Text.Pandoc.Lua.Util (addField, dofileWithTraceback)
3129import Text.Pandoc.Options
3230import Text.Pandoc.Templates (renderTemplate)
33−import qualified Text.Pandoc.UTF8 as UTF8
3431import Text.Pandoc.Writers.Shared
3532
3633import qualified Foreign.Lua as Lua
@@ -81,11 +78,6 @@ instance (Pushable a, Pushable b) => Pushable (KeyValue a b) where
8178 Lua.push v
8279 Lua.rawset (Lua.nthFromTop 3)
8380
84−data PandocLuaException = PandocLuaException Text
85− deriving (Show, Typeable)
86−
87−instance Exception PandocLuaException
88−
8981-- | Convert Pandoc to custom markup.
9082writeCustom :: FilePath -> WriterOptions -> Pandoc -> PandocIO Text
9183writeCustom luaFile opts doc@(Pandoc meta _) = do
@@ -97,21 +89,20 @@ writeCustom luaFile opts doc@(Pandoc meta _) = do
9789 stat <- dofileWithTraceback luaFile
9890 -- check for error in lua script (later we'll change the return type
9991 -- to handle this more gracefully):
100− when (stat /= Lua.OK) $
101− Lua.tostring' (-1) >>= throw . PandocLuaException . UTF8.toText
92+ when (stat /= Lua.OK)
93+ Lua.throwTopMessage
10294 rendered <- docToCustom opts doc
10395 context <- metaToContext opts
10496 (fmap (literal . pack) . blockListToCustom)
10597 (fmap (literal . pack) . inlineListToCustom)
10698 meta
10799 return (pack rendered, context)
108− let (body, context) = case res of
109− Left (LuaException msg) -> throw (PandocLuaException msg)
110− Right x -> x
111− return $
112− case writerTemplate opts of
113− Nothing -> body
114− Just tpl -> render Nothing $
100+ case res of
101+ Left msg -> throw msg
102+ Right (body, context) -> return $
103+ case writerTemplate opts of
104+ Nothing -> body
105+ Just tpl -> render Nothing $
115106 renderTemplate tpl $ setField "body" body context
116107
117108docToCustom :: WriterOptions -> Pandoc -> Lua String
stack.yaml+1
@@ -23,6 +23,7 @@ extra-deps:
2323- regex-pcre-builtin-0.95.0.8.8.35
2424- doclayout-0.3
2525- emojis-0.1
26+- hslua-1.1.0
2627- jira-wiki-markup-1.3.0
2728- HsYAML-0.2.0.0
2829- HsYAML-aeson-0.2.0.0
test/Tests/Lua.hs+8−7
@@ -1,5 +1,6 @@
11{-# LANGUAGE NoImplicitPrelude #-}
22{-# LANGUAGE OverloadedStrings #-}
3+{-# LANGUAGE ScopedTypeVariables #-}
34{- |
45 Module : Tests.Lua
56 Copyright : © 2017-2020 Albert Krewinkel
@@ -28,11 +29,13 @@ import Text.Pandoc.Builder (bulletList, definitionList, displayMath, divWith,
2829import Text.Pandoc.Class (runIOorExplode, setUserDataDir)
2930import Text.Pandoc.Definition (Block (BlockQuote, Div, Para), Inline (Emph, Str),
3031 Attr, Meta, Pandoc, pandocTypesVersion)
32+import Text.Pandoc.Error (PandocError (PandocLuaError))
3133import Text.Pandoc.Filter (Filter (LuaFilter), applyFilters)
3234import Text.Pandoc.Lua (runLua)
3335import Text.Pandoc.Options (def)
3436import Text.Pandoc.Shared (pandocVersion)
3537
38+import qualified Control.Monad.Catch as Catch
3639import qualified Foreign.Lua as Lua
3740import qualified Data.Text as T
3841import qualified Data.Text.Encoding as TE
@@ -197,12 +200,13 @@ tests = map (localOption (QuickCheckTests 20))
197200
198201 , testCase "informative error messages" . runLuaTest $ do
199202 Lua.pushboolean True
200− err <- Lua.peekEither Lua.stackTop
201− case (err :: Either String Pandoc) of
202− Left msg -> do
203+ eitherPandoc <- Catch.try (Lua.peek Lua.stackTop :: Lua.Lua Pandoc)
204+ case eitherPandoc of
205+ Left (PandocLuaError msg) -> do
203206 let expectedMsg = "Could not get Pandoc value: "
204207 <> "table expected, got boolean"
205208 Lua.liftIO $ assertEqual "unexpected error message" expectedMsg msg
209+ Left e -> error ("Expected a Lua error, but got " <> show e)
206210 Right _ -> error "Getting a Pandoc element from a bool should fail."
207211 ]
208212
@@ -223,10 +227,7 @@ roundtripEqual x = (x ==) <$> roundtripped
223227 size <- Lua.gettop
224228 when (size - oldSize /= 1) $
225229 error ("not exactly one additional element on the stack: " ++ show size)
226− res <- Lua.peekEither (-1)
227− case res of
228− Left e -> error (show e)
229− Right y -> return y
230+ Lua.peek (-1)
230231
231232runLuaTest :: Lua.Lua a -> IO a
232233runLuaTest op = runIOorExplode $ do
test/lua/module/pandoc-types.lua+3−4
@@ -26,10 +26,9 @@ return {
2626 )
2727 end),
2828 test('non-version string is rejected', function ()
29− assert.error_matches(
30− function () Version '11friends' end,
31− '11friends'
32− )
29+ local success, msg = pcall(function () Version '11friends' end)
30+ assert.is_falsy(success)
31+ assert.is_truthy(tostring(msg):match('11friends'))
3332 end)
3433 },
3534