aboutsummaryrefslogtreecommitdiff
path: root/pandoc-lua-engine/src
diff options
context:
space:
mode:
authorAlbert Krewinkel <[email protected]>2022-09-29 17:24:31 +0200
committerJohn MacFarlane <[email protected]>2022-09-30 08:33:40 -0700
commit5be9052f5fb7283372b3d5497bef499718a34992 (patch)
tree80e5805786ef7ab08f363135861e1aa9c8868f6f /pandoc-lua-engine/src
parent79980eee4a1854921d7fb8b14848894b53cc21a7 (diff)
[API Change] Extract Lua code into new package pandoc-lua-engine
The flag 'lua53` must now be used with that package if pandoc is to be compiled against Lua 5.3.
Diffstat (limited to 'pandoc-lua-engine/src')
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua.hs52
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/ErrorConversion.hs40
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Filter.hs81
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Global.hs74
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Init.hs167
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/CommonState.hs68
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/Context.hs28
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/PandocError.hs50
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/ReaderOptions.hs137
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/Reference.hs96
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/Sources.hs56
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/Template.hs31
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/WriterOptions.hs244
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Module/MediaBag.hs142
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Pandoc.hs320
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Module/System.hs41
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Template.hs61
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Types.hs42
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Utils.hs239
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Orphans.hs116
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/PandocLua.hs113
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Reader.hs84
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Writer.hs63
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Writer/Classic.hs250
24 files changed, 2595 insertions, 0 deletions
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua.hs
new file mode 100644
index 000000000..d6134fc01
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua.hs
@@ -0,0 +1,52 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeApplications #-}
+{- |
+ Module : Text.Pandoc.Lua
+ Copyright : Copyright © 2017-2022 Albert Krewinkel
+ License : GNU GPL, version 2 or above
+
+ Maintainer : Albert Krewinkel <[email protected]>
+ Stability : alpha
+
+Running pandoc Lua filters.
+-}
+module Text.Pandoc.Lua
+ ( -- * High-level functions
+ applyFilter
+ , readCustom
+ , writeCustom
+ -- * Low-level functions
+ , Global(..)
+ , setGlobals
+ , runLua
+ , runLuaNoEnv
+ -- * Engine
+ , getEngine
+ ) where
+
+import Control.Monad.IO.Class (MonadIO (liftIO))
+import HsLua.Core (getglobal, openlibs, run, top, tostring)
+import Text.Pandoc.Error (PandocError)
+import Text.Pandoc.Lua.Filter (applyFilter)
+import Text.Pandoc.Lua.Global (Global (..), setGlobals)
+import Text.Pandoc.Lua.Init (runLua, runLuaNoEnv)
+import Text.Pandoc.Lua.Reader (readCustom)
+import Text.Pandoc.Lua.Writer (writeCustom)
+import Text.Pandoc.Lua.Orphans ()
+import Text.Pandoc.Scripting (ScriptingEngine (..))
+import qualified Text.Pandoc.UTF8 as UTF8
+
+-- | Constructs the Lua scripting engine.
+getEngine :: MonadIO m => m ScriptingEngine
+getEngine = do
+ versionName <- liftIO . run @PandocError $ do
+ openlibs
+ getglobal "_VERSION"
+ tostring top
+ pure $ ScriptingEngine
+ { engineName = maybe "Lua (unknown version)" UTF8.toText versionName
+ , engineApplyFilter = applyFilter
+ , engineReadCustom = readCustom
+ , engineWriteCustom = writeCustom
+ }
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/ErrorConversion.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/ErrorConversion.hs
new file mode 100644
index 000000000..3968eba84
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/ErrorConversion.hs
@@ -0,0 +1,40 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{- |
+ Module : Text.Pandoc.Lua.ErrorConversion
+ Copyright : © 2020-2022 Albert Krewinkel
+ License : GNU GPL, version 2 or above
+
+ Maintainer : Albert Krewinkel <[email protected]>
+ Stability : alpha
+
+Define how Lua errors are converted into @'PandocError'@ Haskell
+exceptions, and /vice versa/.
+-}
+module Text.Pandoc.Lua.ErrorConversion
+ ( addContextToException
+ ) where
+
+import HsLua (LuaError, LuaE, resultToEither, runPeek, top)
+import Text.Pandoc.Error (PandocError (PandocLuaError))
+import Text.Pandoc.Lua.Marshal.PandocError (pushPandocError, peekPandocError)
+
+import qualified Data.Text as T
+import qualified HsLua as Lua
+
+addContextToException :: ()
+addContextToException = undefined
+
+-- | Retrieve a @'PandocError'@ from the Lua stack.
+popPandocError :: LuaE PandocError PandocError
+popPandocError = do
+ errResult <- runPeek $ peekPandocError top
+ case resultToEither errResult of
+ Right x -> return x
+ Left err -> return $ PandocLuaError (T.pack err)
+
+-- Ensure conversions between Lua errors and 'PandocError' exceptions
+-- are possible.
+instance LuaError PandocError where
+ popException = popPandocError
+ pushException = pushPandocError
+ luaException = PandocLuaError . T.pack
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Filter.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Filter.hs
new file mode 100644
index 000000000..c019095f8
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Filter.hs
@@ -0,0 +1,81 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE IncoherentInstances #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{- |
+Module : Text.Pandoc.Lua.Filter
+Copyright : © 2012-2022 John MacFarlane,
+ © 2017-2022 Albert Krewinkel
+License : GNU GPL, version 2 or above
+Maintainer : Albert Krewinkel <[email protected]>
+Stability : alpha
+
+Types and functions for running Lua filters.
+-}
+module Text.Pandoc.Lua.Filter
+ ( applyFilter
+ ) where
+import Control.Monad ((>=>), (<$!>))
+import HsLua as Lua
+import Text.Pandoc.Definition
+import Text.Pandoc.Filter (Environment (..))
+import Text.Pandoc.Lua.ErrorConversion ()
+import Text.Pandoc.Lua.Marshal.AST
+import Text.Pandoc.Lua.Marshal.Filter
+import Text.Pandoc.Lua.Global (Global (..), setGlobals)
+import Text.Pandoc.Lua.Init (runLua)
+import Control.Exception (throw)
+import qualified Data.Text as T
+import Text.Pandoc.Class (PandocMonad)
+import Control.Monad.Trans (MonadIO)
+import Text.Pandoc.Error (PandocError (PandocFilterError, PandocLuaError))
+
+-- | Transform document using the filter defined in the given file.
+runFilterFile :: FilePath -> Pandoc -> LuaE PandocError Pandoc
+runFilterFile filterPath doc = do
+ oldtop <- gettop
+ stat <- dofileTrace filterPath
+ if stat /= Lua.OK
+ then throwErrorAsException
+ else do
+ newtop <- gettop
+ -- Use the returned filters, or the implicitly defined global
+ -- filter if nothing was returned.
+ luaFilters <- forcePeek $
+ if newtop - oldtop >= 1
+ then peekList peekFilter top
+ else (:[]) <$!> (liftLua pushglobaltable *> peekFilter top)
+ settop oldtop
+ runAll luaFilters doc
+
+runAll :: [Filter] -> Pandoc -> LuaE PandocError Pandoc
+runAll = foldr ((>=>) . applyFully) return
+
+-- | Run the Lua filter in @filterPath@ for a transformation to the
+-- target format (first element in args). Pandoc uses Lua init files to
+-- setup the Lua interpreter.
+applyFilter :: (PandocMonad m, MonadIO m)
+ => Environment
+ -> [String]
+ -> FilePath
+ -> Pandoc
+ -> m Pandoc
+applyFilter fenv args fp doc = do
+ let globals = [ FORMAT $ case args of
+ x:_ -> T.pack x
+ _ -> ""
+ , PANDOC_READER_OPTIONS (envReaderOptions fenv)
+ , PANDOC_WRITER_OPTIONS (envWriterOptions fenv)
+ , PANDOC_SCRIPT_FILE fp
+ ]
+ runLua >=> forceResult fp $ do
+ setGlobals globals
+ runFilterFile fp doc
+
+forceResult :: (PandocMonad m, MonadIO m)
+ => FilePath -> Either PandocError Pandoc -> m Pandoc
+forceResult fp eitherResult = case eitherResult of
+ Right x -> return x
+ Left err -> throw . PandocFilterError (T.pack fp) $ case err of
+ PandocLuaError msg -> msg
+ _ -> T.pack $ show err
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Global.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Global.hs
new file mode 100644
index 000000000..702ba7bd5
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Global.hs
@@ -0,0 +1,74 @@
+{-# LANGUAGE OverloadedStrings #-}
+{- |
+ Module : Text.Pandoc.Lua
+ Copyright : Copyright © 2017-2022 Albert Krewinkel
+ License : GNU GPL, version 2 or above
+
+ Maintainer : Albert Krewinkel <[email protected]>
+ Stability : alpha
+
+Pandoc's Lua globals.
+-}
+module Text.Pandoc.Lua.Global
+ ( Global (..)
+ , setGlobals
+ ) where
+
+import HsLua as Lua
+import HsLua.Module.Version (pushVersion)
+import Text.Pandoc.Class (CommonState)
+import Text.Pandoc.Definition (Pandoc, pandocTypesVersion)
+import Text.Pandoc.Error (PandocError)
+import Text.Pandoc.Lua.Marshal.CommonState (pushCommonState)
+import Text.Pandoc.Lua.Marshal.Pandoc (pushPandoc)
+import Text.Pandoc.Lua.Marshal.ReaderOptions (pushReaderOptionsReadonly)
+import Text.Pandoc.Lua.Marshal.WriterOptions (pushWriterOptions)
+import Text.Pandoc.Lua.Orphans ()
+import Text.Pandoc.Options (ReaderOptions, WriterOptions)
+import Text.Pandoc.Shared (pandocVersion)
+
+import qualified Data.Text as Text
+
+-- | Permissible global Lua variables.
+data Global =
+ FORMAT Text.Text
+ | PANDOC_API_VERSION
+ | PANDOC_DOCUMENT Pandoc
+ | PANDOC_READER_OPTIONS ReaderOptions
+ | PANDOC_WRITER_OPTIONS WriterOptions
+ | PANDOC_SCRIPT_FILE FilePath
+ | PANDOC_STATE CommonState
+ | PANDOC_VERSION
+ -- Cannot derive instance of Data because of CommonState
+
+-- | Set all given globals.
+setGlobals :: [Global] -> LuaE PandocError ()
+setGlobals = mapM_ setGlobal
+
+setGlobal :: Global -> LuaE PandocError ()
+setGlobal global = case global of
+ -- This could be simplified if Global was an instance of Data.
+ FORMAT format -> do
+ Lua.pushText format
+ Lua.setglobal "FORMAT"
+ PANDOC_API_VERSION -> do
+ pushVersion pandocTypesVersion
+ Lua.setglobal "PANDOC_API_VERSION"
+ PANDOC_DOCUMENT doc -> do
+ pushPandoc doc
+ Lua.setglobal "PANDOC_DOCUMENT"
+ PANDOC_READER_OPTIONS ropts -> do
+ pushReaderOptionsReadonly ropts
+ Lua.setglobal "PANDOC_READER_OPTIONS"
+ PANDOC_WRITER_OPTIONS wopts -> do
+ pushWriterOptions wopts
+ Lua.setglobal "PANDOC_WRITER_OPTIONS"
+ PANDOC_SCRIPT_FILE filePath -> do
+ Lua.pushString filePath
+ Lua.setglobal "PANDOC_SCRIPT_FILE"
+ PANDOC_STATE commonState -> do
+ pushCommonState commonState
+ Lua.setglobal "PANDOC_STATE"
+ PANDOC_VERSION -> do
+ pushVersion pandocVersion
+ Lua.setglobal "PANDOC_VERSION"
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Init.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Init.hs
new file mode 100644
index 000000000..caa490d52
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Init.hs
@@ -0,0 +1,167 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{- |
+ Module : Text.Pandoc.Lua
+ Copyright : Copyright © 2017-2022 Albert Krewinkel
+ License : GNU GPL, version 2 or above
+
+ Maintainer : Albert Krewinkel <[email protected]>
+ Stability : alpha
+
+Functions to initialize the Lua interpreter.
+-}
+module Text.Pandoc.Lua.Init
+ ( runLua
+ , runLuaNoEnv
+ ) where
+
+import Control.Monad (forM, forM_, when)
+import Control.Monad.Catch (throwM, try)
+import Control.Monad.Trans (MonadIO (..))
+import Data.Maybe (catMaybes)
+import HsLua as Lua hiding (status, try)
+import Text.Pandoc.Class (PandocMonad, readDataFile)
+import Text.Pandoc.Error (PandocError (PandocLuaError))
+import Text.Pandoc.Lua.Marshal.List (newListMetatable, pushListModule)
+import Text.Pandoc.Lua.PandocLua (PandocLua, liftPandocLua, runPandocLua)
+import qualified Data.ByteString.Char8 as Char8
+import qualified Data.Text as T
+import qualified Lua.LPeg as LPeg
+import qualified HsLua.Aeson
+import qualified HsLua.Module.DocLayout as Module.Layout
+import qualified HsLua.Module.Path as Module.Path
+import qualified HsLua.Module.Text as Module.Text
+import qualified Text.Pandoc.Lua.Module.Pandoc as Module.Pandoc
+import qualified Text.Pandoc.Lua.Module.MediaBag as Pandoc.MediaBag
+import qualified Text.Pandoc.Lua.Module.System as Pandoc.System
+import qualified Text.Pandoc.Lua.Module.Template as Pandoc.Template
+import qualified Text.Pandoc.Lua.Module.Types as Pandoc.Types
+import qualified Text.Pandoc.Lua.Module.Utils as Pandoc.Utils
+
+-- | Run the lua interpreter, using pandoc's default way of environment
+-- initialization.
+runLua :: (PandocMonad m, MonadIO m)
+ => LuaE PandocError a -> m (Either PandocError a)
+runLua action =
+ runPandocLua . try $ do
+ initLuaState
+ liftPandocLua action
+
+-- | Like 'runLua', but ignores all environment variables like @LUA_PATH@.
+runLuaNoEnv :: (PandocMonad m, MonadIO m)
+ => LuaE PandocError a -> m (Either PandocError a)
+runLuaNoEnv action =
+ runPandocLua . try $ do
+ liftPandocLua $ do
+ -- This is undocumented, but works -- the code is adapted from the
+ -- `lua.c` sources for the default interpreter.
+ Lua.pushboolean True
+ Lua.setfield Lua.registryindex "LUA_NOENV"
+ initLuaState
+ liftPandocLua action
+
+-- | Modules that are loaded at startup and assigned to fields in the
+-- pandoc module.
+--
+-- Note that @pandoc.List@ is not included here for technical reasons;
+-- it must be handled separately.
+loadedModules :: [Module PandocError]
+loadedModules =
+ [ Pandoc.MediaBag.documentedModule
+ , Pandoc.System.documentedModule
+ , Pandoc.Template.documentedModule
+ , Pandoc.Types.documentedModule
+ , Pandoc.Utils.documentedModule
+ , Module.Layout.documentedModule { moduleName = "pandoc.layout" }
+ , Module.Path.documentedModule { moduleName = "pandoc.path" }
+ , Module.Text.documentedModule
+ ]
+
+-- | Initialize the lua state with all required values
+initLuaState :: PandocLua ()
+initLuaState = do
+ liftPandocLua Lua.openlibs
+ initJsonMetatable
+ initPandocModule
+ installLpegSearcher
+ setGlobalModules
+ loadInitScript "init.lua"
+ where
+ initPandocModule :: PandocLua ()
+ initPandocModule = liftPandocLua $ do
+ -- Push module table
+ registerModule Module.Pandoc.documentedModule
+ -- load modules and add them to the `pandoc` module table.
+ forM_ loadedModules $ \mdl -> do
+ registerModule mdl
+ let fieldname (Name mdlname) = Name .
+ maybe mdlname snd . Char8.uncons . snd $
+ Char8.break (== '.') mdlname
+ Lua.setfield (nth 2) (fieldname $ moduleName mdl)
+ -- pandoc.List is low-level and must be opened differently.
+ requirehs "pandoc.List" (const pushListModule)
+ setfield (nth 2) "List"
+ -- assign module to global variable
+ Lua.setglobal "pandoc"
+
+ loadInitScript :: FilePath -> PandocLua ()
+ loadInitScript scriptFile = do
+ script <- readDataFile scriptFile
+ status <- liftPandocLua $ Lua.dostring script
+ when (status /= Lua.OK) . liftPandocLua $ do
+ err <- popException
+ let prefix = "Couldn't load '" <> T.pack scriptFile <> "':\n"
+ throwM . PandocLuaError . (prefix <>) $ case err of
+ PandocLuaError msg -> msg
+ _ -> T.pack $ show err
+
+ setGlobalModules :: PandocLua ()
+ setGlobalModules = liftPandocLua $ do
+ let globalModules =
+ [ ("lpeg", LPeg.luaopen_lpeg_ptr) -- must be loaded first
+ , ("re", LPeg.luaopen_re_ptr) -- re depends on lpeg
+ ]
+ loadedBuiltInModules <- fmap catMaybes . forM globalModules $
+ \(pkgname, luaopen) -> do
+ Lua.pushcfunction luaopen
+ usedBuiltIn <- Lua.pcall 0 1 Nothing >>= \case
+ OK -> do -- all good, loading succeeded
+ -- register as loaded module so later modules can rely on this
+ Lua.getfield Lua.registryindex Lua.loaded
+ Lua.pushvalue (Lua.nth 2)
+ Lua.setfield (Lua.nth 2) pkgname
+ Lua.pop 1 -- pop _LOADED
+ return True
+ _ -> do -- built-in library failed, load system lib
+ Lua.pop 1 -- ignore error message
+ -- Try loading via the normal package loading mechanism.
+ Lua.getglobal "require"
+ Lua.pushName pkgname
+ Lua.call 1 1 -- Throws an exception if loading failed again!
+ return False
+
+ -- Module on top of stack. Register as global
+ Lua.setglobal pkgname
+ return $ if usedBuiltIn then Just pkgname else Nothing
+
+ -- Remove module entry from _LOADED table in registry if we used a
+ -- built-in library. This ensures that later calls to @require@ will
+ -- prefer the shared library, if any.
+ forM_ loadedBuiltInModules $ \pkgname -> do
+ Lua.getfield Lua.registryindex Lua.loaded
+ Lua.pushnil
+ Lua.setfield (Lua.nth 2) pkgname
+ Lua.pop 1 -- registry
+
+ installLpegSearcher :: PandocLua ()
+ installLpegSearcher = liftPandocLua $ do
+ Lua.getglobal' "package.searchers"
+ Lua.pushHaskellFunction $ Lua.state >>= liftIO . LPeg.lpeg_searcher
+ Lua.rawseti (Lua.nth 2) . (+1) . fromIntegral =<< Lua.rawlen (Lua.nth 2)
+ Lua.pop 1 -- remove 'package.searchers' from stack
+
+-- | Setup the metatable that's assigned to Lua tables that were created
+-- from/via JSON arrays.
+initJsonMetatable :: PandocLua ()
+initJsonMetatable = liftPandocLua $ do
+ newListMetatable HsLua.Aeson.jsonarray (pure ())
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/CommonState.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/CommonState.hs
new file mode 100644
index 000000000..74ce69887
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/CommonState.hs
@@ -0,0 +1,68 @@
+{-# LANGUAGE OverloadedStrings #-}
+{- |
+ Module : Text.Pandoc.Lua.Marshal.CommonState
+ Copyright : © 2012-2022 John MacFarlane
+ © 2017-2022 Albert Krewinkel
+ License : GNU GPL, version 2 or above
+ Maintainer : Albert Krewinkel <[email protected]>
+ Stability : alpha
+
+Instances to marshal (push) and unmarshal (peek) the common state.
+-}
+module Text.Pandoc.Lua.Marshal.CommonState
+ ( typeCommonState
+ , peekCommonState
+ , pushCommonState
+ ) where
+
+import HsLua
+import Text.Pandoc.Class (CommonState (..))
+import Text.Pandoc.Logging (LogMessage, showLogMessage)
+import Text.Pandoc.Lua.Marshal.List (pushPandocList)
+
+-- | Lua type used for the @CommonState@ object.
+typeCommonState :: LuaError e => DocumentedType e CommonState
+typeCommonState = deftype "pandoc CommonState" []
+ [ readonly "input_files" "input files passed to pandoc"
+ (pushPandocList pushString, stInputFiles)
+
+ , readonly "output_file" "the file to which pandoc will write"
+ (maybe pushnil pushString, stOutputFile)
+
+ , readonly "log" "list of log messages"
+ (pushPandocList (pushUD typeLogMessage), stLog)
+
+ , readonly "request_headers" "headers to add for HTTP requests"
+ (pushPandocList (pushPair pushText pushText), stRequestHeaders)
+
+ , readonly "resource_path"
+ "path to search for resources like included images"
+ (pushPandocList pushString, stResourcePath)
+
+ , readonly "source_url" "absolute URL + dir of 1st source file"
+ (maybe pushnil pushText, stSourceURL)
+
+ , readonly "user_data_dir" "directory to search for data files"
+ (maybe pushnil pushString, stUserDataDir)
+
+ , readonly "trace" "controls whether tracing messages are issued"
+ (pushBool, stTrace)
+
+ , readonly "verbosity" "verbosity level"
+ (pushString . show, stVerbosity)
+ ]
+
+peekCommonState :: LuaError e => Peeker e CommonState
+peekCommonState = peekUD typeCommonState
+
+pushCommonState :: LuaError e => Pusher e CommonState
+pushCommonState = pushUD typeCommonState
+
+typeLogMessage :: LuaError e => DocumentedType e LogMessage
+typeLogMessage = deftype "pandoc LogMessage"
+ [ operation Index $ defun "__tostring"
+ ### liftPure showLogMessage
+ <#> udparam typeLogMessage "msg" "object"
+ =#> functionResult pushText "string" "stringified log message"
+ ]
+ mempty -- no members
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/Context.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/Context.hs
new file mode 100644
index 000000000..126f3a82d
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/Context.hs
@@ -0,0 +1,28 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{- |
+ Module : Text.Pandoc.Lua.Marshaling.Context
+ Copyright : © 2012-2022 John MacFarlane
+ © 2017-2022 Albert Krewinkel
+ License : GNU GPL, version 2 or above
+
+ Maintainer : Albert Krewinkel <[email protected]>
+ Stability : alpha
+
+Marshaling instance for doctemplates Context and its components.
+-}
+module Text.Pandoc.Lua.Marshal.Context () where
+
+import qualified HsLua as Lua
+import HsLua (Pushable)
+import Text.DocTemplates (Context(..), Val(..), TemplateTarget)
+import Text.DocLayout (render)
+
+instance (TemplateTarget a, Pushable a) => Pushable (Context a) where
+ push (Context m) = Lua.push m
+
+instance (TemplateTarget a, Pushable a) => Pushable (Val a) where
+ push NullVal = Lua.push ()
+ push (BoolVal b) = Lua.push b
+ push (MapVal ctx) = Lua.push ctx
+ push (ListVal xs) = Lua.push xs
+ push (SimpleVal d) = Lua.push $ render Nothing d
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/PandocError.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/PandocError.hs
new file mode 100644
index 000000000..7f83f2fc0
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/PandocError.hs
@@ -0,0 +1,50 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+{- |
+ Module : Text.Pandoc.Lua.Marshal.PandocError
+ Copyright : © 2020-2022 Albert Krewinkel
+ License : GNU GPL, version 2 or above
+
+ Maintainer : Albert Krewinkel <[email protected]>
+ Stability : alpha
+
+Marshal of @'PandocError'@ values.
+-}
+module Text.Pandoc.Lua.Marshal.PandocError
+ ( peekPandocError
+ , pushPandocError
+ , typePandocError
+ )
+ where
+
+import HsLua (LuaError, Peeker, Pusher, liftLua, pushString)
+import HsLua.Packaging
+import Text.Pandoc.Error (PandocError (PandocLuaError))
+
+import qualified HsLua as Lua
+import qualified Text.Pandoc.UTF8 as UTF8
+
+-- | Lua userdata type definition for PandocError.
+typePandocError :: LuaError e => DocumentedType e PandocError
+typePandocError = deftype "PandocError"
+ [ operation Tostring $ defun "__tostring"
+ ### liftPure (show @PandocError)
+ <#> udparam typePandocError "obj" "PandocError object"
+ =#> functionResult pushString "string" "string representation of error."
+ ]
+ mempty -- no members
+
+-- | Peek a @'PandocError'@ element to the Lua stack.
+pushPandocError :: LuaError e => Pusher e PandocError
+pushPandocError = pushUD typePandocError
+
+-- | Retrieve a @'PandocError'@ from the Lua stack.
+peekPandocError :: LuaError e => Peeker e PandocError
+peekPandocError idx = Lua.retrieving "PandocError" $
+ liftLua (Lua.ltype idx) >>= \case
+ Lua.TypeUserdata -> peekUD typePandocError idx
+ _ -> do
+ msg <- liftLua $ Lua.state >>= \l -> Lua.liftIO (Lua.popErrorMessage l)
+ return $ PandocLuaError (UTF8.toText msg)
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/ReaderOptions.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/ReaderOptions.hs
new file mode 100644
index 000000000..bec7d81bf
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/ReaderOptions.hs
@@ -0,0 +1,137 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{- |
+ Module : Text.Pandoc.Lua.Marshaling.ReaderOptions
+ Copyright : © 2012-2022 John MacFarlane
+ © 2017-2022 Albert Krewinkel
+ License : GNU GPL, version 2 or above
+
+ Maintainer : Albert Krewinkel <[email protected]>
+ Stability : alpha
+
+Marshaling instance for ReaderOptions and its components.
+-}
+module Text.Pandoc.Lua.Marshal.ReaderOptions
+ ( peekReaderOptions
+ , pushReaderOptions
+ , pushReaderOptionsReadonly
+ ) where
+
+import Data.Default (def)
+import HsLua as Lua
+#if !MIN_VERSION_hslua(2,2,0)
+import HsLua.Aeson (peekViaJSON, pushViaJSON)
+#endif
+import Text.Pandoc.Lua.Marshal.List (pushPandocList)
+import Text.Pandoc.Options (ReaderOptions (..))
+
+--
+-- Reader Options
+--
+
+-- | Retrieve a ReaderOptions value, either from a normal ReaderOptions
+-- value, from a read-only object, or from a table with the same
+-- keys as a ReaderOptions object.
+peekReaderOptions :: LuaError e => Peeker e ReaderOptions
+peekReaderOptions = retrieving "ReaderOptions" . \idx ->
+ liftLua (ltype idx) >>= \case
+ TypeUserdata -> choice [ peekUD typeReaderOptions
+ , peekUD typeReaderOptionsReadonly
+ ]
+ idx
+ TypeTable -> peekReaderOptionsTable idx
+ _ -> failPeek =<<
+ typeMismatchMessage "ReaderOptions userdata or table" idx
+
+-- | Pushes a ReaderOptions value as userdata object.
+pushReaderOptions :: LuaError e => Pusher e ReaderOptions
+pushReaderOptions = pushUD typeReaderOptions
+
+-- | Pushes a ReaderOptions object, but makes it read-only.
+pushReaderOptionsReadonly :: LuaError e => Pusher e ReaderOptions
+pushReaderOptionsReadonly = pushUD typeReaderOptionsReadonly
+
+-- | ReaderOptions object type for read-only values.
+typeReaderOptionsReadonly :: LuaError e => DocumentedType e ReaderOptions
+typeReaderOptionsReadonly = deftype "ReaderOptions (read-only)"
+ [ operation Tostring $ lambda
+ ### liftPure show
+ <#> udparam typeReaderOptions "opts" "options to print in native format"
+ =#> functionResult pushString "string" "Haskell representation"
+ , operation Newindex $ lambda
+ ### (failLua "This ReaderOptions value is read-only.")
+ =?> "Throws an error when called, i.e., an assignment is made."
+ ]
+ readerOptionsMembers
+
+-- | 'ReaderOptions' object type.
+typeReaderOptions :: LuaError e => DocumentedType e ReaderOptions
+typeReaderOptions = deftype "ReaderOptions"
+ [ operation Tostring $ lambda
+ ### liftPure show
+ <#> udparam typeReaderOptions "opts" "options to print in native format"
+ =#> functionResult pushString "string" "Haskell representation"
+ ]
+ readerOptionsMembers
+
+-- | Member properties of 'ReaderOptions' Lua values.
+readerOptionsMembers :: LuaError e
+ => [Member e (DocumentedFunction e) ReaderOptions]
+readerOptionsMembers =
+ [ property "abbreviations" ""
+ (pushSet pushText, readerAbbreviations)
+ (peekSet peekText, \opts x -> opts{ readerAbbreviations = x })
+ , property "columns" ""
+ (pushIntegral, readerColumns)
+ (peekIntegral, \opts x -> opts{ readerColumns = x })
+ , property "default_image_extension" ""
+ (pushText, readerDefaultImageExtension)
+ (peekText, \opts x -> opts{ readerDefaultImageExtension = x })
+ , property "extensions" ""
+ (pushViaJSON, readerExtensions)
+ (peekViaJSON, \opts x -> opts{ readerExtensions = x })
+ , property "indented_code_classes" ""
+ (pushPandocList pushText, readerIndentedCodeClasses)
+ (peekList peekText, \opts x -> opts{ readerIndentedCodeClasses = x })
+ , property "standalone" ""
+ (pushBool, readerStandalone)
+ (peekBool, \opts x -> opts{ readerStandalone = x })
+ , property "strip_comments" ""
+ (pushBool, readerStripComments)
+ (peekBool, \opts x -> opts{ readerStripComments = x })
+ , property "tab_stop" ""
+ (pushIntegral, readerTabStop)
+ (peekIntegral, \opts x -> opts{ readerTabStop = x })
+ , property "track_changes" ""
+ (pushViaJSON, readerTrackChanges)
+ (choice [peekRead, peekViaJSON], \opts x -> opts{ readerTrackChanges = x })
+ ]
+
+-- | Retrieves a 'ReaderOptions' object from a table on the stack, using
+-- the default values for all missing fields.
+--
+-- Internally, this pushes the default reader options, sets each
+-- key/value pair of the table in the userdata value, then retrieves the
+-- object again. This will update all fields and complain about unknown
+-- keys.
+peekReaderOptionsTable :: LuaError e => Peeker e ReaderOptions
+peekReaderOptionsTable idx = retrieving "ReaderOptions (table)" $ do
+ liftLua $ do
+ absidx <- absindex idx
+ pushUD typeReaderOptions def
+ let setFields = do
+ next absidx >>= \case
+ False -> return () -- all fields were copied
+ True -> do
+ pushvalue (nth 2) *> insert (nth 2)
+ settable (nth 4) -- set in userdata object
+ setFields
+ pushnil -- first key
+ setFields
+ peekUD typeReaderOptions top `lastly` pop 1
+
+instance Pushable ReaderOptions where
+ push = pushReaderOptions
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/Reference.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/Reference.hs
new file mode 100644
index 000000000..c23bfef9f
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/Reference.hs
@@ -0,0 +1,96 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{- |
+ Module : Text.Pandoc.Lua.Marshaling.ReaderOptions
+ Copyright : © 2012-2022 John MacFarlane
+ © 2017-2022 Albert Krewinkel
+ License : GNU GPL, version 2 or above
+
+ Maintainer : Albert Krewinkel <[email protected]>
+ Stability : alpha
+
+Marshal citeproc 'Reference' values.
+-}
+module Text.Pandoc.Lua.Marshal.Reference
+ ( pushReference
+ ) where
+
+import Citeproc.Types
+ ( Date (..), DateParts (..), ItemId (..), Name (..), Reference (..)
+ , Val (..), Variable, fromVariable
+ )
+import Control.Monad (forM_)
+import HsLua hiding (Name, Reference, pushName, peekName)
+import Text.Pandoc.Builder (Inlines, toList)
+import Text.Pandoc.Lua.Marshal.Inline (pushInlines)
+import Text.Pandoc.Lua.Marshal.List (pushPandocList)
+
+import qualified Data.Map as Map
+
+-- | Pushes a ReaderOptions value as userdata object.
+pushReference :: LuaError e => Pusher e (Reference Inlines)
+pushReference reference = do
+ pushAsTable [ ("id", pushItemId . referenceId)
+ , ("type", pushText . referenceType)
+ ]
+ reference
+ forM_ (Map.toList $ referenceVariables reference) $ \(var, val) -> do
+ pushVariable var
+ pushVal val
+ rawset (nth 3)
+
+-- | Pushes an 'ItemId' as a string.
+pushItemId :: Pusher e ItemId
+pushItemId = pushText . unItemId
+
+-- | Pushes a person's 'Name' as a table.
+pushName :: LuaError e => Pusher e Name
+pushName = pushAsTable
+ [ ("family" , pushTextOrNil . nameFamily)
+ , ("given" , pushTextOrNil . nameGiven)
+ , ("dropping-particle" , pushTextOrNil . nameDroppingParticle)
+ , ("non-dropping-particle" , pushTextOrNil . nameNonDroppingParticle)
+ , ("suffix" , pushTextOrNil . nameSuffix)
+ , ("literal" , pushTextOrNil . nameLiteral)
+ , ("comma-suffix" , pushBoolOrNil . nameCommaSuffix)
+ , ("static-ordering" , pushBoolOrNil . nameStaticOrdering)
+ ]
+ where
+ pushTextOrNil = \case
+ Nothing -> pushnil
+ Just xs -> pushText xs
+
+-- | Pushes a boolean, but uses @nil@ instead of @false@; table fields
+-- are not set unless the value is true.
+pushBoolOrNil :: Pusher e Bool
+pushBoolOrNil = \case
+ False -> pushnil
+ True -> pushBool True
+
+-- | Pushes a 'Variable' as string.
+pushVariable :: Pusher e Variable
+pushVariable = pushText . fromVariable
+
+-- | Pushes a 'Val', i.e., a variable value.
+pushVal :: LuaError e => Pusher e (Val Inlines)
+pushVal = \case
+ TextVal t -> pushText t
+ FancyVal inlns -> pushInlines $ toList inlns
+ NumVal i -> pushIntegral i
+ NamesVal names -> pushPandocList pushName names
+ DateVal date -> pushDate date
+ _ -> pushText mempty
+
+-- | Pushes a 'Date' as table.
+pushDate :: LuaError e => Pusher e Date
+pushDate = pushAsTable
+ [ ("date-parts", pushPandocList pushDateParts . dateParts)
+ , ("circa", pushBoolOrNil . dateCirca)
+ , ("season", maybe pushnil pushIntegral . dateSeason)
+ , ("literal", maybe pushnil pushText . dateLiteral)
+ ]
+ where
+ -- date parts are lists of Int values
+ pushDateParts (DateParts dp) = pushPandocList pushIntegral dp
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/Sources.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/Sources.hs
new file mode 100644
index 000000000..3b3b58329
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/Sources.hs
@@ -0,0 +1,56 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{- |
+Module : Text.Pandoc.Lua.Marshaling.Sources
+Copyright : © 2021-2022 Albert Krewinkel
+License : GNU GPL, version 2 or above
+Maintainer : Albert Krewinkel <[email protected]>
+
+Marshal 'Sources'.
+-}
+module Text.Pandoc.Lua.Marshal.Sources
+ ( peekSources
+ , pushSources
+ ) where
+
+import Control.Monad ((<$!>))
+import Data.Text (Text)
+import HsLua as Lua
+import Text.Pandoc.Lua.Marshal.List (newListMetatable)
+import Text.Pandoc.Sources (Sources (..), toSources)
+import Text.Parsec (SourcePos, sourceName)
+
+-- | Pushes the 'Sources' as a list of lazy Lua objects.
+pushSources :: LuaError e => Pusher e Sources
+pushSources (Sources srcs) = do
+ pushList (pushUD typeSource) srcs
+ newListMetatable "pandoc Sources" $ do
+ pushName "__tostring"
+ pushHaskellFunction $ do
+ sources <- forcePeek $ peekList (peekUD typeSource) (nthBottom 1)
+ pushText . mconcat $ map snd sources
+ return 1
+ rawset (nth 3)
+ setmetatable (nth 2)
+
+-- | Retrieves sources from the stack.
+peekSources :: LuaError e => Peeker e Sources
+peekSources idx = liftLua (ltype idx) >>= \case
+ TypeString -> toSources <$!> peekText idx
+ TypeTable -> Sources <$!> peekList (peekUD typeSource) idx
+ _ -> Sources . (:[]) <$!> peekUD typeSource idx
+
+-- | Source object type.
+typeSource :: LuaError e => DocumentedType e (SourcePos, Text)
+typeSource = deftype "pandoc input source"
+ [ operation Tostring $ lambda
+ ### liftPure snd
+ <#> udparam typeSource "srcs" "Source to print in native format"
+ =#> functionResult pushText "string" "Haskell representation"
+ ]
+ [ readonly "name" "source name"
+ (pushString, sourceName . fst)
+ , readonly "text" "source text"
+ (pushText, snd)
+ ]
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/Template.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/Template.hs
new file mode 100644
index 000000000..56878b109
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/Template.hs
@@ -0,0 +1,31 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{- |
+Module : Text.Pandoc.Lua.Marshal.Template
+Copyright : © 2021-2022 Albert Krewinkel
+License : GNU GPL, version 2 or above
+Maintainer : Albert Krewinkel <[email protected]>
+
+Marshal 'Template' 'Text'.
+-}
+module Text.Pandoc.Lua.Marshal.Template
+ ( pushTemplate
+ , peekTemplate
+ , typeTemplate
+ ) where
+
+import Data.Text (Text)
+import HsLua as Lua
+import Text.DocTemplates (Template)
+
+-- | Pushes a 'Template' as a an opaque userdata value.
+pushTemplate :: LuaError e => Pusher e (Template Text)
+pushTemplate = pushUD typeTemplate
+
+-- | Retrieves a 'Template' 'Text' value from the stack.
+peekTemplate :: LuaError e => Peeker e (Template Text)
+peekTemplate = peekUD typeTemplate
+
+-- | Template object type.
+typeTemplate :: LuaError e => DocumentedType e (Template Text)
+typeTemplate = deftype "pandoc Template" [] []
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/WriterOptions.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/WriterOptions.hs
new file mode 100644
index 000000000..86df682c5
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/WriterOptions.hs
@@ -0,0 +1,244 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{- |
+ Module : Text.Pandoc.Lua.Marshaling.WriterOptions
+ Copyright : © 2021-2022 Albert Krewinkel, John MacFarlane
+ License : GNU GPL, version 2 or above
+
+ Maintainer : Albert Krewinkel <[email protected]>
+ Stability : alpha
+
+Marshaling instance for WriterOptions and its components.
+-}
+module Text.Pandoc.Lua.Marshal.WriterOptions
+ ( peekWriterOptions
+ , pushWriterOptions
+ ) where
+
+import Control.Applicative (optional)
+import Data.Default (def)
+import HsLua as Lua
+#if !MIN_VERSION_hslua(2,2,0)
+import HsLua.Aeson (peekViaJSON, pushViaJSON)
+#endif
+import Text.Pandoc.Lua.Marshal.List (pushPandocList)
+import Text.Pandoc.Lua.Marshal.Template (peekTemplate, pushTemplate)
+import Text.Pandoc.Options (WriterOptions (..))
+
+--
+-- Writer Options
+--
+
+-- | Retrieve a WriterOptions value, either from a normal WriterOptions
+-- value, from a read-only object, or from a table with the same
+-- keys as a WriterOptions object.
+peekWriterOptions :: LuaError e => Peeker e WriterOptions
+peekWriterOptions = retrieving "WriterOptions" . \idx ->
+ liftLua (ltype idx) >>= \case
+ TypeUserdata -> peekUD typeWriterOptions idx
+ TypeTable -> peekWriterOptionsTable idx
+ _ -> failPeek =<<
+ typeMismatchMessage "WriterOptions userdata or table" idx
+
+-- | Pushes a WriterOptions value as userdata object.
+pushWriterOptions :: LuaError e => Pusher e WriterOptions
+pushWriterOptions = pushUD typeWriterOptions
+
+-- | 'WriterOptions' object type.
+typeWriterOptions :: LuaError e => DocumentedType e WriterOptions
+typeWriterOptions = deftype "WriterOptions"
+ [ operation Tostring $ lambda
+ ### liftPure show
+ <#> udparam typeWriterOptions "opts" "options to print in native format"
+ =#> functionResult pushString "string" "Haskell representation"
+ ]
+ [ property "cite_method"
+ "How to print cites"
+ (pushViaJSON, writerCiteMethod)
+ (peekViaJSON, \opts x -> opts{ writerCiteMethod = x })
+
+ , property "columns"
+ "Characters in a line (for text wrapping)"
+ (pushIntegral, writerColumns)
+ (peekIntegral, \opts x -> opts{ writerColumns = x })
+
+ , property "dpi"
+ "DPI for pixel to/from inch/cm conversions"
+ (pushIntegral, writerDpi)
+ (peekIntegral, \opts x -> opts{ writerDpi = x })
+
+ , property "email_obfuscation"
+ "How to obfuscate emails"
+ (pushViaJSON, writerEmailObfuscation)
+ (peekViaJSON, \opts x -> opts{ writerEmailObfuscation = x })
+
+ , property "epub_chapter_level"
+ "Header level for chapters (separate files)"
+ (pushIntegral, writerEpubChapterLevel)
+ (peekIntegral, \opts x -> opts{ writerEpubChapterLevel = x })
+
+ , property "epub_fonts"
+ "Paths to fonts to embed"
+ (pushPandocList pushString, writerEpubFonts)
+ (peekList peekString, \opts x -> opts{ writerEpubFonts = x })
+
+ , property "epub_metadata"
+ "Metadata to include in EPUB"
+ (maybe pushnil pushText, writerEpubMetadata)
+ (optional . peekText, \opts x -> opts{ writerEpubMetadata = x })
+
+ , property "epub_subdirectory"
+ "Subdir for epub in OCF"
+ (pushText, writerEpubSubdirectory)
+ (peekText, \opts x -> opts{ writerEpubSubdirectory = x })
+
+ , property "extensions"
+ "Markdown extensions that can be used"
+ (pushViaJSON, writerExtensions)
+ (peekViaJSON, \opts x -> opts{ writerExtensions = x })
+
+ , property "highlight_style"
+ "Style to use for highlighting (nil = no highlighting)"
+ (maybe pushnil pushViaJSON, writerHighlightStyle)
+ (optional . peekViaJSON, \opts x -> opts{ writerHighlightStyle = x })
+
+ , property "html_math_method"
+ "How to print math in HTML"
+ (pushViaJSON, writerHTMLMathMethod)
+ (peekViaJSON, \opts x -> opts{ writerHTMLMathMethod = x })
+
+ , property "html_q_tags"
+ "Use @<q>@ tags for quotes in HTML"
+ (pushBool, writerHtmlQTags)
+ (peekBool, \opts x -> opts{ writerHtmlQTags = x })
+
+ , property "identifier_prefix"
+ "Prefix for section & note ids in HTML and for footnote marks in markdown"
+ (pushText, writerIdentifierPrefix)
+ (peekText, \opts x -> opts{ writerIdentifierPrefix = x })
+
+ , property "incremental"
+ "True if lists should be incremental"
+ (pushBool, writerIncremental)
+ (peekBool, \opts x -> opts{ writerIncremental = x })
+
+ , property "listings"
+ "Use listings package for code"
+ (pushBool, writerListings)
+ (peekBool, \opts x -> opts{ writerListings = x })
+
+ , property "number_offset"
+ "Starting number for section, subsection, ..."
+ (pushPandocList pushIntegral, writerNumberOffset)
+ (peekList peekIntegral, \opts x -> opts{ writerNumberOffset = x })
+
+ , property "number_sections"
+ "Number sections in LaTeX"
+ (pushBool, writerNumberSections)
+ (peekBool, \opts x -> opts{ writerNumberSections = x })
+
+ , property "prefer_ascii"
+ "Prefer ASCII representations of characters when possible"
+ (pushBool, writerPreferAscii)
+ (peekBool, \opts x -> opts{ writerPreferAscii = x })
+
+ , property "reference_doc"
+ "Path to reference document if specified"
+ (maybe pushnil pushString, writerReferenceDoc)
+ (optional . peekString, \opts x -> opts{ writerReferenceDoc = x })
+
+ , property "reference_links"
+ "Use reference links in writing markdown, rst"
+ (pushBool, writerReferenceLinks)
+ (peekBool, \opts x -> opts{ writerReferenceLinks = x })
+
+ , property "reference_location"
+ "Location of footnotes and references for writing markdown"
+ (pushViaJSON, writerReferenceLocation)
+ (peekViaJSON, \opts x -> opts{ writerReferenceLocation = x })
+
+ , property "section_divs"
+ "Put sections in div tags in HTML"
+ (pushBool, writerSectionDivs)
+ (peekBool, \opts x -> opts{ writerSectionDivs = x })
+
+ , property "setext_headers"
+ "Use setext headers for levels 1-2 in markdown"
+ (pushBool, writerSetextHeaders)
+ (peekBool, \opts x -> opts{ writerSetextHeaders = x })
+
+ , property "slide_level"
+ "Force header level of slides"
+ (maybe pushnil pushIntegral, writerSlideLevel)
+ (optional . peekIntegral, \opts x -> opts{ writerSlideLevel = x })
+
+ -- , property "syntax_map" "Syntax highlighting definition"
+ -- (pushViaJSON, writerSyntaxMap)
+ -- (peekViaJSON, \opts x -> opts{ writerSyntaxMap = x })
+ -- :: SyntaxMap
+
+ , property "tab_stop"
+ "Tabstop for conversion btw spaces and tabs"
+ (pushIntegral, writerTabStop)
+ (peekIntegral, \opts x -> opts{ writerTabStop = x })
+
+ , property "table_of_contents"
+ "Include table of contents"
+ (pushBool, writerTableOfContents)
+ (peekBool, \opts x -> opts{ writerTableOfContents = x })
+
+ , property "template"
+ "Template to use"
+ (maybe pushnil pushTemplate, writerTemplate)
+ (optional . peekTemplate, \opts x -> opts{ writerTemplate = x })
+ -- :: Maybe (Template Text)
+
+ , property "toc_depth"
+ "Number of levels to include in TOC"
+ (pushIntegral, writerTOCDepth)
+ (peekIntegral, \opts x -> opts{ writerTOCDepth = x })
+
+ , property "top_level_division"
+ "Type of top-level divisions"
+ (pushViaJSON, writerTopLevelDivision)
+ (peekViaJSON, \opts x -> opts{ writerTopLevelDivision = x })
+
+ , property "variables"
+ "Variables to set in template"
+ (pushViaJSON, writerVariables)
+ (peekViaJSON, \opts x -> opts{ writerVariables = x })
+
+ , property "wrap_text"
+ "Option for wrapping text"
+ (pushViaJSON, writerWrapText)
+ (peekViaJSON, \opts x -> opts{ writerWrapText = x })
+ ]
+
+-- | Retrieves a 'WriterOptions' object from a table on the stack, using
+-- the default values for all missing fields.
+--
+-- Internally, this pushes the default writer options, sets each
+-- key/value pair of the table in the userdata value, then retrieves the
+-- object again. This will update all fields and complain about unknown
+-- keys.
+peekWriterOptionsTable :: LuaError e => Peeker e WriterOptions
+peekWriterOptionsTable idx = retrieving "WriterOptions (table)" $ do
+ liftLua $ do
+ absidx <- absindex idx
+ pushUD typeWriterOptions def
+ let setFields = do
+ next absidx >>= \case
+ False -> return () -- all fields were copied
+ True -> do
+ pushvalue (nth 2) *> insert (nth 2)
+ settable (nth 4) -- set in userdata object
+ setFields
+ pushnil -- first key
+ setFields
+ peekUD typeWriterOptions top `lastly` pop 1
+
+instance Pushable WriterOptions where
+ push = pushWriterOptions
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/MediaBag.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/MediaBag.hs
new file mode 100644
index 000000000..ca028f444
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/MediaBag.hs
@@ -0,0 +1,142 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{- |
+ Module : Text.Pandoc.Lua.Module.MediaBag
+ Copyright : Copyright © 2017-2022 Albert Krewinkel
+ License : GNU GPL, version 2 or above
+ Maintainer : Albert Krewinkel <[email protected]>
+
+The Lua module @pandoc.mediabag@.
+-}
+module Text.Pandoc.Lua.Module.MediaBag
+ ( documentedModule
+ ) where
+
+import Prelude hiding (lookup)
+import Data.Maybe (fromMaybe)
+import HsLua ( LuaE, DocumentedFunction, Module (..)
+ , (<#>), (###), (=#>), (=?>), (#?), defun, functionResult
+ , opt, parameter, stringParam, textParam)
+import Text.Pandoc.Class ( CommonState (..), fetchItem, fillMediaBag
+ , getMediaBag, modifyCommonState, setMediaBag)
+import Text.Pandoc.Error (PandocError)
+import Text.Pandoc.Lua.Marshal.Pandoc (peekPandoc, pushPandoc)
+import Text.Pandoc.Lua.Marshal.List (pushPandocList)
+import Text.Pandoc.Lua.Orphans ()
+import Text.Pandoc.Lua.PandocLua (unPandocLua)
+import Text.Pandoc.MIME (MimeType)
+
+import qualified Data.ByteString.Lazy as BL
+import qualified HsLua as Lua
+import qualified Text.Pandoc.MediaBag as MB
+
+--
+-- MediaBag submodule
+--
+documentedModule :: Module PandocError
+documentedModule = Module
+ { moduleName = "pandoc.mediabag"
+ , moduleDescription = "mediabag access"
+ , moduleFields = []
+ , moduleFunctions =
+ [ delete
+ , empty
+ , fetch
+ , fill
+ , insert
+ , items
+ , list
+ , lookup
+ ]
+ , moduleOperations = []
+ }
+
+-- | Delete a single item from the media bag.
+delete :: DocumentedFunction PandocError
+delete = defun "delete"
+ ### (\fp -> unPandocLua $ modifyCommonState
+ (\st -> st { stMediaBag = MB.deleteMedia fp (stMediaBag st) }))
+ <#> stringParam "filepath" "filename of item to delete"
+ =#> []
+
+
+-- | Delete all items from the media bag.
+empty :: DocumentedFunction PandocError
+empty = defun "empty"
+ ### unPandocLua (modifyCommonState (\st -> st { stMediaBag = mempty }))
+ =#> []
+
+-- | Fill the mediabag with all images in the document that aren't
+-- present yet.
+fill :: DocumentedFunction PandocError
+fill = defun "fill"
+ ### unPandocLua . fillMediaBag
+ <#> parameter peekPandoc "Pandoc" "doc"
+ "document from which to fill the mediabag"
+ =#> functionResult pushPandoc "Pandoc" "modified document"
+ #? ("Fills the mediabag with the images in the given document.\n" <>
+ "An image that cannot be retrieved will be replaced with a Span\n" <>
+ "of class \"image\" that contains the image description.\n" <>
+ "" <>
+ "Images for which the mediabag already contains an item will\n" <>
+ "not be processed again.")
+
+-- | Insert a new item into the media bag.
+insert :: DocumentedFunction PandocError
+insert = defun "insert"
+ ### (\fp mmime contents -> unPandocLua $ do
+ mb <- getMediaBag
+ setMediaBag $ MB.insertMedia fp mmime contents mb
+ return (Lua.NumResults 0))
+ <#> stringParam "filepath" "item file path"
+ <#> opt (textParam "mimetype" "the item's MIME type")
+ <#> parameter Lua.peekLazyByteString "string" "contents" "binary contents"
+ =#> []
+
+-- | Returns iterator values to be used with a Lua @for@ loop.
+items :: DocumentedFunction PandocError
+items = defun "items"
+ ### (do
+ mb <-unPandocLua getMediaBag
+ let pushItem (fp, mimetype, contents) = do
+ Lua.pushString fp
+ Lua.pushText mimetype
+ Lua.pushByteString $ BL.toStrict contents
+ return (Lua.NumResults 3)
+ Lua.pushIterator pushItem (MB.mediaItems mb))
+ =?> "Iterator triple"
+
+-- | Function to lookup a value in the mediabag.
+lookup :: DocumentedFunction PandocError
+lookup = defun "lookup"
+ ### (\fp -> unPandocLua (MB.lookupMedia fp <$> getMediaBag) >>= \case
+ Nothing -> 1 <$ Lua.pushnil
+ Just item -> 2 <$ do
+ Lua.pushText $ MB.mediaMimeType item
+ Lua.pushLazyByteString $ MB.mediaContents item)
+ <#> stringParam "filepath" "path of item to lookup"
+ =?> "MIME type and contents"
+
+-- | Function listing all mediabag items.
+list :: DocumentedFunction PandocError
+list = defun "list"
+ ### (unPandocLua (MB.mediaDirectory <$> getMediaBag))
+ =#> functionResult (pushPandocList pushEntry) "table" "list of entry triples"
+ where
+ pushEntry :: (FilePath, MimeType, Int) -> LuaE PandocError ()
+ pushEntry (fp, mimeType, contentLength) = do
+ Lua.newtable
+ Lua.pushName "path" *> Lua.pushString fp *> Lua.rawset (-3)
+ Lua.pushName "type" *> Lua.pushText mimeType *> Lua.rawset (-3)
+ Lua.pushName "length" *> Lua.pushIntegral contentLength *> Lua.rawset (-3)
+
+-- | Lua function to retrieve a new item.
+fetch :: DocumentedFunction PandocError
+fetch = defun "fetch"
+ ### (\src -> do
+ (bs, mimeType) <- unPandocLua $ fetchItem src
+ Lua.pushText $ fromMaybe "" mimeType
+ Lua.pushByteString bs
+ return 2)
+ <#> textParam "src" "URI to fetch"
+ =?> "Returns two string values: the fetched contents and the mimetype."
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Pandoc.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Pandoc.hs
new file mode 100644
index 000000000..e708f4345
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Pandoc.hs
@@ -0,0 +1,320 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+{- |
+ Module : Text.Pandoc.Lua.Module.Pandoc
+ Copyright : Copyright © 2017-2022 Albert Krewinkel
+ License : GNU GPL, version 2 or above
+
+ Maintainer : Albert Krewinkel <[email protected]>
+ Stability : alpha
+
+Pandoc module for lua.
+-}
+module Text.Pandoc.Lua.Module.Pandoc
+ ( pushModule
+ , documentedModule
+ ) where
+
+import Prelude hiding (read)
+import Control.Applicative ((<|>))
+import Control.Monad (forM_, when)
+import Control.Monad.Catch (catch, throwM)
+import Data.Data (Data, dataTypeConstrs, dataTypeOf, showConstr)
+import Data.Default (Default (..))
+import Data.Maybe (fromMaybe)
+import Data.Proxy (Proxy (Proxy))
+import HsLua hiding (pushModule)
+import System.Exit (ExitCode (..))
+import Text.Pandoc.Definition
+import Text.Pandoc.Error (PandocError (..))
+import Text.Pandoc.Lua.Orphans ()
+import Text.Pandoc.Lua.Marshal.AST
+import Text.Pandoc.Lua.Marshal.Filter (peekFilter)
+import Text.Pandoc.Lua.Marshal.ReaderOptions ( peekReaderOptions
+ , pushReaderOptions)
+import Text.Pandoc.Lua.Marshal.Sources (peekSources)
+import Text.Pandoc.Lua.Marshal.WriterOptions ( peekWriterOptions
+ , pushWriterOptions)
+import Text.Pandoc.Lua.Module.Utils (sha1)
+import Text.Pandoc.Lua.PandocLua (PandocLua (unPandocLua), liftPandocLua)
+import Text.Pandoc.Options ( ReaderOptions (readerExtensions)
+ , WriterOptions (writerExtensions) )
+import Text.Pandoc.Process (pipeProcess)
+import Text.Pandoc.Readers (Reader (..), getReader, readers)
+import Text.Pandoc.Sources (toSources)
+import Text.Pandoc.Writers (Writer (..), getWriter, writers)
+
+import qualified HsLua as Lua
+import qualified Data.ByteString.Lazy as BL
+import qualified Data.ByteString.Lazy.Char8 as BSL
+import qualified Data.Set as Set
+import qualified Data.Text as T
+import qualified Text.Pandoc.UTF8 as UTF8
+
+-- | Push the "pandoc" package to the Lua stack. Requires the `List`
+-- module to be loadable.
+pushModule :: PandocLua NumResults
+pushModule = do
+ liftPandocLua $ Lua.pushModule documentedModule
+ return 1
+
+documentedModule :: Module PandocError
+documentedModule = Module
+ { moduleName = "pandoc"
+ , moduleDescription = T.unlines
+ [ "Lua functions for pandoc scripts; includes constructors for"
+ , "document elements, functions to parse text in a given"
+ , "format, and functions to filter and modify a subtree."
+ ]
+ , moduleFields = readersField : writersField :
+ stringConstants ++ [inlineField, blockField]
+ , moduleOperations = []
+ , moduleFunctions = mconcat
+ [ functions
+ , otherConstructors
+ , blockConstructors
+ , inlineConstructors
+ , metaValueConstructors
+ ]
+ }
+
+-- | Set of input formats accepted by @read@.
+readersField :: Field PandocError
+readersField = Field
+ { fieldName = "readers"
+ , fieldDescription = T.unlines
+ [ "Set of formats that pandoc can parse. All keys in this table can"
+ , "be used as the `format` value in `pandoc.read`."
+ ]
+ , fieldPushValue = pushSet pushText $
+ Set.fromList (map fst (readers @PandocLua))
+ }
+
+-- | Set of input formats accepted by @write@.
+writersField :: Field PandocError
+writersField = Field
+ { fieldName = "writers"
+ , fieldDescription = T.unlines
+ [ "Set of formats that pandoc can generate. All keys in this table"
+ , "can be used as the `format` value in `pandoc.write`."
+ ]
+ , fieldPushValue = pushSet pushText $
+ Set.fromList (map fst (writers @PandocLua))
+ }
+
+-- | Inline table field
+inlineField :: Field PandocError
+inlineField = Field
+ { fieldName = "Inline"
+ , fieldDescription = "Inline constructors, nested under 'constructors'."
+ -- the nesting happens for historical reasons and should probably be
+ -- changed.
+ , fieldPushValue = pushWithConstructorsSubtable inlineConstructors
+ }
+
+-- | @Block@ module field
+blockField :: Field PandocError
+blockField = Field
+ { fieldName = "Block"
+ , fieldDescription = "Inline constructors, nested under 'constructors'."
+ -- the nesting happens for historical reasons and should probably be
+ -- changed.
+ , fieldPushValue = pushWithConstructorsSubtable blockConstructors
+ }
+
+pushWithConstructorsSubtable :: [DocumentedFunction PandocError]
+ -> LuaE PandocError ()
+pushWithConstructorsSubtable constructors = do
+ newtable -- Field table
+ newtable -- constructor table
+ pushName "constructor" *> pushvalue (nth 2) *> rawset (nth 4)
+ forM_ constructors $ \fn -> do
+ pushName (functionName fn)
+ pushDocumentedFunction fn
+ rawset (nth 3)
+ pop 1 -- pop constructor table
+
+otherConstructors :: LuaError e => [DocumentedFunction e]
+otherConstructors =
+ [ mkPandoc
+ , mkMeta
+ , mkAttr
+ , mkAttributeList
+ , mkBlocks
+ , mkCitation
+ , mkCell
+ , mkRow
+ , mkTableHead
+ , mkTableFoot
+ , mkInlines
+ , mkListAttributes
+ , mkSimpleTable
+
+ , defun "ReaderOptions"
+ ### liftPure id
+ <#> parameter peekReaderOptions "ReaderOptions|table" "opts" "reader options"
+ =#> functionResult pushReaderOptions "ReaderOptions" "new object"
+ #? "Creates a new ReaderOptions value."
+
+ , defun "WriterOptions"
+ ### liftPure id
+ <#> parameter peekWriterOptions "WriterOptions|table" "opts"
+ "writer options"
+ =#> functionResult pushWriterOptions "WriterOptions" "new object"
+ #? "Creates a new WriterOptions value."
+ ]
+
+stringConstants :: [Field e]
+stringConstants =
+ let constrs :: forall a. Data a => Proxy a -> [String]
+ constrs _ = map showConstr . dataTypeConstrs . dataTypeOf @a $ undefined
+ nullaryConstructors = mconcat
+ [ constrs (Proxy @ListNumberStyle)
+ , constrs (Proxy @ListNumberDelim)
+ , constrs (Proxy @QuoteType)
+ , constrs (Proxy @MathType)
+ , constrs (Proxy @Alignment)
+ , constrs (Proxy @CitationMode)
+ ]
+ toField s = Field
+ { fieldName = T.pack s
+ , fieldDescription = T.pack s
+ , fieldPushValue = pushString s
+ }
+ in map toField nullaryConstructors
+
+functions :: [DocumentedFunction PandocError]
+functions =
+ [ defun "pipe"
+ ### (\command args input -> do
+ (ec, output) <- Lua.liftIO $ pipeProcess Nothing command args input
+ `catch` (throwM . PandocIOError "pipe")
+ case ec of
+ ExitSuccess -> 1 <$ Lua.pushLazyByteString output
+ ExitFailure n -> do
+ pushPipeError (PipeError (T.pack command) n output)
+ Lua.error)
+ <#> parameter peekString "string" "command" "path to executable"
+ <#> parameter (peekList peekString) "{string,...}" "args"
+ "list of arguments"
+ <#> parameter peekLazyByteString "string" "input"
+ "input passed to process via stdin"
+ =?> "output string, or error triple"
+
+ , defun "read"
+ ### (\content mformatspec mreaderOptions -> do
+ let formatSpec = fromMaybe "markdown" mformatspec
+ readerOpts = fromMaybe def mreaderOptions
+ readAction = getReader formatSpec >>= \case
+ (TextReader r, es) ->
+ r readerOpts{readerExtensions = es}
+ (case content of
+ Left bs -> toSources $ UTF8.toText bs
+ Right sources -> sources)
+ (ByteStringReader r, es) ->
+ case content of
+ Left bs -> r readerOpts{readerExtensions = es}
+ (BSL.fromStrict bs)
+ Right _ -> liftPandocLua $ Lua.failLua
+ "Cannot use bytestring reader with Sources"
+ try (unPandocLua readAction) >>= \case
+ Right pd ->
+ -- success, got a Pandoc document
+ return pd
+ Left (PandocUnknownReaderError f) ->
+ Lua.failLua . T.unpack $ "Unknown reader: " <> f
+ Left (PandocUnsupportedExtensionError e f) ->
+ Lua.failLua . T.unpack $
+ "Extension " <> e <> " not supported for " <> f
+ Left e ->
+ throwM e)
+ <#> parameter (\idx -> (Left <$> peekByteString idx)
+ <|> (Right <$> peekSources idx))
+ "string|Sources" "content" "text to parse"
+ <#> opt (textParam "formatspec" "format and extensions")
+ <#> opt (parameter peekReaderOptions "ReaderOptions" "reader_options"
+ "reader options")
+ =#> functionResult pushPandoc "Pandoc" "result document"
+
+ , sha1
+
+ , defun "walk_block"
+ ### walkElement
+ <#> parameter peekBlockFuzzy "Block" "block" "element to traverse"
+ <#> parameter peekFilter "Filter" "lua_filter" "filter functions"
+ =#> functionResult pushBlock "Block" "modified Block"
+
+ , defun "walk_inline"
+ ### walkElement
+ <#> parameter peekInlineFuzzy "Inline" "inline" "element to traverse"
+ <#> parameter peekFilter "Filter" "lua_filter" "filter functions"
+ =#> functionResult pushInline "Inline" "modified Inline"
+
+ , defun "write"
+ ### (\doc mformatspec mwriterOpts -> do
+ let formatSpec = fromMaybe "html" mformatspec
+ writerOpts = fromMaybe def mwriterOpts
+ unPandocLua $ getWriter formatSpec >>= \case
+ (TextWriter w, es) -> Right <$>
+ w writerOpts{ writerExtensions = es } doc
+ (ByteStringWriter w, es) -> Left <$>
+ w writerOpts{ writerExtensions = es } doc)
+ <#> parameter peekPandoc "Pandoc" "doc" "document to convert"
+ <#> opt (textParam "formatspec" "format and extensions")
+ <#> opt (parameter peekWriterOptions "WriterOptions" "writer_options"
+ "writer options")
+ =#> functionResult (either pushLazyByteString pushText) "string"
+ "result document"
+ ]
+ where
+ walkElement x f =
+ walkInlineSplicing f x
+ >>= walkInlinesStraight f
+ >>= walkBlockSplicing f
+ >>= walkBlocksStraight f
+
+data PipeError = PipeError
+ { pipeErrorCommand :: T.Text
+ , pipeErrorCode :: Int
+ , pipeErrorOutput :: BL.ByteString
+ }
+
+peekPipeError :: LuaError e => StackIndex -> LuaE e PipeError
+peekPipeError idx =
+ PipeError
+ <$> (Lua.getfield idx "command" *> Lua.peek (-1) <* Lua.pop 1)
+ <*> (Lua.getfield idx "error_code" *> Lua.peek (-1) <* Lua.pop 1)
+ <*> (Lua.getfield idx "output" *> Lua.peek (-1) <* Lua.pop 1)
+
+pushPipeError :: LuaError e => Pusher e PipeError
+pushPipeError pipeErr = do
+ pushAsTable [ ("command" , pushText . pipeErrorCommand)
+ , ("error_code" , pushIntegral . pipeErrorCode)
+ , ("output" , pushLazyByteString . pipeErrorOutput)
+ ] pipeErr
+ pushPipeErrorMetaTable
+ Lua.setmetatable (nth 2)
+ where
+ pushPipeErrorMetaTable :: LuaError e => LuaE e ()
+ pushPipeErrorMetaTable = do
+ v <- Lua.newmetatable "pandoc pipe error"
+ when v $ do
+ pushName "__tostring"
+ pushHaskellFunction pipeErrorMessage
+ rawset (nth 3)
+
+ pipeErrorMessage :: LuaError e => LuaE e NumResults
+ pipeErrorMessage = do
+ (PipeError cmd errorCode output) <- peekPipeError (nthBottom 1)
+ pushByteString . BSL.toStrict . BSL.concat $
+ [ BSL.pack "Error running "
+ , BSL.pack $ T.unpack cmd
+ , BSL.pack " (error code "
+ , BSL.pack $ show errorCode
+ , BSL.pack "): "
+ , if output == mempty then BSL.pack "<no output>" else output
+ ]
+ return (NumResults 1)
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/System.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/System.hs
new file mode 100644
index 000000000..70ef1b315
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/System.hs
@@ -0,0 +1,41 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{- |
+ Module : Text.Pandoc.Lua.Module.System
+ Copyright : © 2019-2022 Albert Krewinkel
+ License : GNU GPL, version 2 or above
+
+ Maintainer : Albert Krewinkel <[email protected]>
+ Stability : alpha
+
+Pandoc's system Lua module.
+-}
+module Text.Pandoc.Lua.Module.System
+ ( documentedModule
+ ) where
+
+import HsLua
+import HsLua.Module.System
+ (arch, env, getwd, ls, mkdir, os, rmdir, with_env, with_tmpdir, with_wd)
+
+-- | Push the pandoc.system module on the Lua stack.
+documentedModule :: LuaError e => Module e
+documentedModule = Module
+ { moduleName = "pandoc.system"
+ , moduleDescription = "system functions"
+ , moduleFields =
+ [ arch
+ , os
+ ]
+ , moduleFunctions =
+ [ setName "environment" env
+ , setName "get_working_directory" getwd
+ , setName "list_directory" ls
+ , setName "make_directory" mkdir
+ , setName "remove_directory" rmdir
+ , setName "with_environment" with_env
+ , setName "with_temporary_directory" with_tmpdir
+ , setName "with_working_directory" with_wd
+ ]
+ , moduleOperations = []
+ }
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Template.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Template.hs
new file mode 100644
index 000000000..967fe31a8
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Template.hs
@@ -0,0 +1,61 @@
+{-# LANGUAGE OverloadedStrings #-}
+{- |
+ Module : Text.Pandoc.Lua.Module.Template
+ Copyright : Copyright © 2022 Albert Krewinkel, John MacFarlane
+ License : GNU GPL, version 2 or above
+ Maintainer : Albert Krewinkel <[email protected]>
+
+Lua module to handle pandoc templates.
+-}
+module Text.Pandoc.Lua.Module.Template
+ ( documentedModule
+ ) where
+
+import HsLua
+import Text.Pandoc.Error (PandocError)
+import Text.Pandoc.Lua.Marshal.Template (pushTemplate)
+import Text.Pandoc.Lua.PandocLua (PandocLua (unPandocLua), liftPandocLua)
+import Text.Pandoc.Templates
+ (compileTemplate, getDefaultTemplate, runWithPartials, runWithDefaultPartials)
+
+import qualified Data.Text as T
+
+-- | The "pandoc.template" module.
+documentedModule :: Module PandocError
+documentedModule = Module
+ { moduleName = "pandoc.template"
+ , moduleDescription = T.unlines
+ [ "Lua functions for pandoc templates."
+ ]
+ , moduleFields = []
+ , moduleOperations = []
+ , moduleFunctions = functions
+ }
+
+-- | Template module functions.
+functions :: [DocumentedFunction PandocError]
+functions =
+ [ defun "compile"
+ ### (\template mfilepath -> unPandocLua $
+ case mfilepath of
+ Just fp -> runWithPartials (compileTemplate fp template)
+ Nothing -> runWithDefaultPartials
+ (compileTemplate "templates/default" template))
+ <#> parameter peekText "string" "template" "template string"
+ <#> opt (stringParam "templ_path" "template path")
+ =#> functionResult (either failLua pushTemplate) "pandoc Template"
+ "compiled template"
+
+ , defun "default"
+ ### (\mformat -> unPandocLua $ do
+ let getFORMAT = liftPandocLua $ do
+ getglobal "FORMAT"
+ forcePeek $ peekText top `lastly` pop 1
+ format <- maybe getFORMAT pure mformat
+ getDefaultTemplate format)
+ <#> opt (textParam "writer"
+ "writer for which the template should be returned.")
+ =#> functionResult pushText "string"
+ "string representation of the writer's default template"
+
+ ]
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Types.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Types.hs
new file mode 100644
index 000000000..b8d45d93e
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Types.hs
@@ -0,0 +1,42 @@
+{-# LANGUAGE OverloadedStrings #-}
+{- |
+ Module : Text.Pandoc.Lua.Module.Types
+ Copyright : © 2019-2022 Albert Krewinkel
+ License : GNU GPL, version 2 or above
+
+ Maintainer : Albert Krewinkel <[email protected]>
+ Stability : alpha
+
+Pandoc data type constructors.
+-}
+module Text.Pandoc.Lua.Module.Types
+ ( documentedModule
+ ) where
+
+import HsLua ( Module (..), (###), (<#>), (=#>)
+ , defun, functionResult, parameter)
+import HsLua.Module.Version (peekVersionFuzzy, pushVersion)
+import Text.Pandoc.Error (PandocError)
+import Text.Pandoc.Lua.ErrorConversion ()
+
+-- | Push the pandoc.types module on the Lua stack.
+documentedModule :: Module PandocError
+documentedModule = Module
+ { moduleName = "pandoc.types"
+ , moduleDescription =
+ "Constructors for types that are not part of the pandoc AST."
+ , moduleFields = []
+ , moduleFunctions =
+ [ defun "Version"
+ ### return
+ <#> parameter peekVersionFuzzy "string|integer|{integer,...}|Version"
+ "version_specifier"
+ (mconcat [ "either a version string like `'2.7.3'`, "
+ , "a single integer like `2`, "
+ , "list of integers like `{2,7,3}`, "
+ , "or a Version object"
+ ])
+ =#> functionResult pushVersion "Version" "A new Version object."
+ ]
+ , moduleOperations = []
+ }
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Utils.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Utils.hs
new file mode 100644
index 000000000..33349870c
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Utils.hs
@@ -0,0 +1,239 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+{- |
+ Module : Text.Pandoc.Lua.Module.Utils
+ Copyright : Copyright © 2017-2022 Albert Krewinkel
+ License : GNU GPL, version 2 or above
+
+ Maintainer : Albert Krewinkel <[email protected]>
+ Stability : alpha
+
+Utility module for Lua, exposing internal helper functions.
+-}
+module Text.Pandoc.Lua.Module.Utils
+ ( documentedModule
+ , sha1
+ ) where
+
+import Control.Applicative ((<|>))
+import Control.Monad ((<$!>))
+import Data.Data (showConstr, toConstr)
+import Data.Default (def)
+import Data.Maybe (fromMaybe)
+import Data.Version (Version)
+import HsLua as Lua
+import HsLua.Module.Version (peekVersionFuzzy, pushVersion)
+import Text.Pandoc.Citeproc (getReferences, processCitations)
+import Text.Pandoc.Definition
+import Text.Pandoc.Error (PandocError)
+import Text.Pandoc.Filter (applyJSONFilter)
+import Text.Pandoc.Lua.Marshal.AST
+import Text.Pandoc.Lua.Marshal.Reference
+import Text.Pandoc.Lua.PandocLua (PandocLua (unPandocLua))
+
+import qualified Data.Digest.Pure.SHA as SHA
+import qualified Data.ByteString.Lazy as BSL
+import qualified Data.Map as Map
+import qualified Data.Text as T
+import qualified Text.Pandoc.Builder as B
+import qualified Text.Pandoc.Shared as Shared
+import qualified Text.Pandoc.UTF8 as UTF8
+import qualified Text.Pandoc.Writers.Shared as Shared
+
+-- | Push the "pandoc.utils" module to the Lua stack.
+documentedModule :: Module PandocError
+documentedModule = Module
+ { moduleName = "pandoc.utils"
+ , moduleDescription = "pandoc utility functions"
+ , moduleFields = []
+ , moduleOperations = []
+ , moduleFunctions =
+ [ defun "blocks_to_inlines"
+ ### (\blks mSep -> do
+ let sep = maybe Shared.defaultBlocksSeparator B.fromList mSep
+ return $ B.toList (Shared.blocksToInlinesWithSep sep blks))
+ <#> parameter (peekList peekBlock) "list of blocks"
+ "blocks" ""
+ <#> opt (parameter (peekList peekInline) "list of inlines" "inline" "")
+ =#> functionResult pushInlines "list of inlines" ""
+
+ , defun "citeproc"
+ ### unPandocLua . processCitations
+ <#> parameter peekPandoc "Pandoc" "doc" "document"
+ =#> functionResult pushPandoc "Pandoc" "processed document"
+ #? T.unwords
+ [ "Process the citations in the file, replacing them with "
+ , "rendered citations and adding a bibliography. "
+ , "See the manual section on citation rendering for details."
+ ]
+
+ , defun "equals"
+ ### equal
+ <#> parameter pure "AST element" "elem1" ""
+ <#> parameter pure "AST element" "elem2" ""
+ =#> functionResult pushBool "boolean" "true iff elem1 == elem2"
+
+ , defun "make_sections"
+ ### liftPure3 Shared.makeSections
+ <#> parameter peekBool "boolean" "numbering" "add header numbers"
+ <#> parameter (\i -> (Nothing <$ peekNil i) <|> (Just <$!> peekIntegral i))
+ "integer or nil" "baselevel" ""
+ <#> parameter (peekList peekBlock) "list of blocks"
+ "blocks" "document blocks to process"
+ =#> functionResult pushBlocks "list of Blocks"
+ "processes blocks"
+
+ , defun "normalize_date"
+ ### liftPure Shared.normalizeDate
+ <#> parameter peekText "string" "date" "the date string"
+ =#> functionResult (maybe pushnil pushText) "string or nil"
+ "normalized date, or nil if normalization failed."
+ #? T.unwords
+ [ "Parse a date and convert (if possible) to \"YYYY-MM-DD\" format. We"
+ , "limit years to the range 1601-9999 (ISO 8601 accepts greater than"
+ , "or equal to 1583, but MS Word only accepts dates starting 1601)."
+ , "Returns nil instead of a string if the conversion failed."
+ ]
+
+ , sha1
+
+ , defun "Version"
+ ### liftPure (id @Version)
+ <#> parameter peekVersionFuzzy
+ "version string, list of integers, or integer"
+ "v" "version description"
+ =#> functionResult pushVersion "Version" "new Version object"
+ #? "Creates a Version object."
+
+ , defun "references"
+ ### (unPandocLua . getReferences Nothing)
+ <#> parameter peekPandoc "Pandoc" "doc" "document"
+ =#> functionResult (pushPandocList pushReference) "table"
+ "lift of references"
+ #? mconcat
+ [ "Get references defined inline in the metadata and via an external "
+ , "bibliography. Only references that are actually cited in the "
+ , "document (either with a genuine citation or with `nocite`) are "
+ , "returned. URL variables are converted to links."
+ ]
+
+ , defun "run_json_filter"
+ ### (\doc filterPath margs -> do
+ args <- case margs of
+ Just xs -> return xs
+ Nothing -> do
+ Lua.getglobal "FORMAT"
+ (forcePeek ((:[]) <$!> peekString top) <* pop 1)
+ applyJSONFilter def args filterPath doc
+ )
+ <#> parameter peekPandoc "Pandoc" "doc" "input document"
+ <#> parameter peekString "filepath" "filter_path" "path to filter"
+ <#> opt (parameter (peekList peekString) "list of strings"
+ "args" "arguments to pass to the filter")
+ =#> functionResult pushPandoc "Pandoc" "filtered document"
+
+ , defun "stringify"
+ ### stringify
+ <#> parameter pure "AST element" "elem" "some pandoc AST element"
+ =#> functionResult pushText "string" "stringified element"
+
+ , defun "from_simple_table"
+ ### from_simple_table
+ <#> parameter peekSimpleTable "SimpleTable" "simple_tbl" ""
+ =?> "Simple table"
+
+ , defun "to_roman_numeral"
+ ### liftPure Shared.toRomanNumeral
+ <#> parameter (peekIntegral @Int) "integer" "n" "number smaller than 4000"
+ =#> functionResult pushText "string" "roman numeral"
+ #? "Converts a number < 4000 to uppercase roman numeral."
+
+ , defun "to_simple_table"
+ ### to_simple_table
+ <#> parameter peekTable "Block" "tbl" "a table"
+ =#> functionResult pushSimpleTable "SimpleTable" "SimpleTable object"
+ #? "Converts a table into an old/simple table."
+
+ , defun "type"
+ ### (\idx -> getmetafield idx "__name" >>= \case
+ TypeString -> fromMaybe mempty <$> tostring top
+ _ -> ltype idx >>= typename)
+ <#> parameter pure "any" "object" ""
+ =#> functionResult pushByteString "string" "type of the given value"
+ #? ("Pandoc-friendly version of Lua's default `type` function, " <>
+ "returning the type of a value. If the argument has a " <>
+ "string-valued metafield `__name`, then it gives that string. " <>
+ "Otherwise it behaves just like the normal `type` function.")
+ ]
+ }
+
+-- | Documented Lua function to compute the hash of a string.
+sha1 :: DocumentedFunction e
+sha1 = defun "sha1"
+ ### liftPure (SHA.showDigest . SHA.sha1)
+ <#> parameter (fmap BSL.fromStrict . peekByteString) "string" "input" ""
+ =#> functionResult pushString "string" "hexadecimal hash value"
+ #? "Compute the hash of the given string value."
+
+
+-- | Convert pandoc structure to a string with formatting removed.
+-- Footnotes are skipped (since we don't want their contents in link
+-- labels).
+stringify :: LuaError e => StackIndex -> LuaE e T.Text
+stringify idx = forcePeek . retrieving "stringifyable element" $
+ choice
+ [ (fmap Shared.stringify . peekPandoc)
+ , (fmap Shared.stringify . peekInline)
+ , (fmap Shared.stringify . peekBlock)
+ , (fmap Shared.stringify . peekCitation)
+ , (fmap stringifyMetaValue . peekMetaValue)
+ , (fmap (const "") . peekAttr)
+ , (fmap (const "") . peekListAttributes)
+ ] idx
+ where
+ stringifyMetaValue :: MetaValue -> T.Text
+ stringifyMetaValue mv = case mv of
+ MetaBool b -> T.toLower $ T.pack (show b)
+ MetaString s -> s
+ MetaList xs -> mconcat $ map stringifyMetaValue xs
+ MetaMap m -> mconcat $ map (stringifyMetaValue . snd) (Map.toList m)
+ _ -> Shared.stringify mv
+
+-- | Converts an old/simple table into a normal table block element.
+from_simple_table :: SimpleTable -> LuaE PandocError NumResults
+from_simple_table (SimpleTable capt aligns widths head' body) = do
+ Lua.push $ Table
+ nullAttr
+ (Caption Nothing [Plain capt | not (null capt)])
+ (zipWith (\a w -> (a, toColWidth w)) aligns widths)
+ (TableHead nullAttr [blockListToRow head' | not (null head') ])
+ [TableBody nullAttr 0 [] $ map blockListToRow body | not (null body)]
+ (TableFoot nullAttr [])
+ return (NumResults 1)
+ where
+ blockListToRow :: [[Block]] -> Row
+ blockListToRow = Row nullAttr . map (B.simpleCell . B.fromList)
+
+ toColWidth :: Double -> ColWidth
+ toColWidth 0 = ColWidthDefault
+ toColWidth w = ColWidth w
+
+-- | Converts a table into an old/simple table.
+to_simple_table :: Block -> LuaE PandocError SimpleTable
+to_simple_table = \case
+ Table _attr caption specs thead tbodies tfoot -> do
+ let (capt, aligns, widths, headers, rows) =
+ Shared.toLegacyTable caption specs thead tbodies tfoot
+ return $ SimpleTable capt aligns widths headers rows
+ blk -> Lua.failLua $ mconcat
+ [ "Expected Table, got ", showConstr (toConstr blk), "." ]
+
+peekTable :: LuaError e => Peeker e Block
+peekTable idx = peekBlock idx >>= \case
+ t@(Table {}) -> return t
+ b -> Lua.failPeek $ mconcat
+ [ "Expected Table, got "
+ , UTF8.fromString $ showConstr (toConstr b)
+ , "." ]
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Orphans.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Orphans.hs
new file mode 100644
index 000000000..62b54d051
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Orphans.hs
@@ -0,0 +1,116 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# LANGUAGE FlexibleInstances #-}
+{- |
+ Module : Text.Pandoc.Lua.Orphans
+ Copyright : © 2012-2022 John MacFarlane
+ © 2017-2022 Albert Krewinkel
+ License : GNU GPL, version 2 or above
+
+ Maintainer : Albert Krewinkel <[email protected]>
+ Stability : alpha
+
+Orphan instances for Lua's Pushable and Peekable type classes.
+-}
+module Text.Pandoc.Lua.Orphans () where
+
+import Data.Version (Version)
+import HsLua
+import HsLua.Module.Version (peekVersionFuzzy)
+import Text.Pandoc.Definition
+import Text.Pandoc.Lua.Marshal.AST
+import Text.Pandoc.Lua.Marshal.CommonState ()
+import Text.Pandoc.Lua.Marshal.Context ()
+import Text.Pandoc.Lua.Marshal.PandocError()
+import Text.Pandoc.Lua.Marshal.ReaderOptions ()
+import Text.Pandoc.Lua.Marshal.Sources (pushSources)
+import Text.Pandoc.Lua.ErrorConversion ()
+import Text.Pandoc.Sources (Sources)
+
+instance Pushable Pandoc where
+ push = pushPandoc
+
+instance Pushable Meta where
+ push = pushMeta
+
+instance Pushable MetaValue where
+ push = pushMetaValue
+
+instance Pushable Block where
+ push = pushBlock
+
+instance {-# OVERLAPPING #-} Pushable [Block] where
+ push = pushBlocks
+
+instance Pushable Alignment where
+ push = pushString . show
+
+instance Pushable CitationMode where
+ push = pushCitationMode
+
+instance Pushable Format where
+ push = pushFormat
+
+instance Pushable ListNumberDelim where
+ push = pushString . show
+
+instance Pushable ListNumberStyle where
+ push = pushString . show
+
+instance Pushable MathType where
+ push = pushMathType
+
+instance Pushable QuoteType where
+ push = pushQuoteType
+
+instance Pushable Cell where
+ push = pushCell
+
+instance Pushable Inline where
+ push = pushInline
+
+instance {-# OVERLAPPING #-} Pushable [Inline] where
+ push = pushInlines
+
+instance Pushable Citation where
+ push = pushCitation
+
+instance Pushable Row where
+ push = pushRow
+
+instance Pushable TableBody where
+ push = pushTableBody
+
+instance Pushable TableFoot where
+ push = pushTableFoot
+
+instance Pushable TableHead where
+ push = pushTableHead
+
+-- These instances exist only for testing. It's a hack to avoid making
+-- the marshalling modules public.
+instance Peekable Inline where
+ safepeek = peekInline
+
+instance Peekable Block where
+ safepeek = peekBlock
+
+instance Peekable Cell where
+ safepeek = peekCell
+
+instance Peekable Meta where
+ safepeek = peekMeta
+
+instance Peekable Pandoc where
+ safepeek = peekPandoc
+
+instance Peekable Row where
+ safepeek = peekRow
+
+instance Peekable Version where
+ safepeek = peekVersionFuzzy
+
+instance {-# OVERLAPPING #-} Peekable Attr where
+ safepeek = peekAttr
+
+instance Pushable Sources where
+ push = pushSources
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/PandocLua.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/PandocLua.hs
new file mode 100644
index 000000000..e07a91d61
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/PandocLua.hs
@@ -0,0 +1,113 @@
+{-# LANGUAGE DeriveFunctor #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{- |
+ Module : Text.Pandoc.Lua.PandocLua
+ Copyright : Copyright © 2020-2022 Albert Krewinkel
+ License : GNU GPL, version 2 or above
+
+ Maintainer : Albert Krewinkel <[email protected]>
+ Stability : alpha
+
+PandocMonad instance which allows execution of Lua operations and which
+uses Lua to handle state.
+-}
+module Text.Pandoc.Lua.PandocLua
+ ( PandocLua (..)
+ , runPandocLua
+ , liftPandocLua
+ ) where
+
+import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
+import Control.Monad.Except (MonadError (catchError, throwError))
+import Control.Monad.IO.Class (MonadIO)
+import HsLua as Lua
+import Text.Pandoc.Class (PandocMonad (..))
+import Text.Pandoc.Error (PandocError)
+import Text.Pandoc.Lua.Global (Global (..), setGlobals)
+import Text.Pandoc.Lua.Marshal.CommonState (peekCommonState)
+
+import qualified Control.Monad.Catch as Catch
+import qualified Text.Pandoc.Class.IO as IO
+
+-- | Type providing access to both, pandoc and Lua operations.
+newtype PandocLua a = PandocLua { unPandocLua :: LuaE PandocError a }
+ deriving
+ ( Applicative
+ , Functor
+ , Monad
+ , MonadCatch
+ , MonadIO
+ , MonadMask
+ , MonadThrow
+ )
+
+-- | Lift a @'Lua'@ operation into the @'PandocLua'@ type.
+liftPandocLua :: LuaE PandocError a -> PandocLua a
+liftPandocLua = PandocLua
+
+-- | Evaluate a @'PandocLua'@ computation, running all contained Lua
+-- operations..
+runPandocLua :: (PandocMonad m, MonadIO m) => PandocLua a -> m a
+runPandocLua pLua = do
+ origState <- getCommonState
+ globals <- defaultGlobals
+ (result, newState) <- liftIO . Lua.run . unPandocLua $ do
+ putCommonState origState
+ liftPandocLua $ setGlobals globals
+ r <- pLua
+ c <- getCommonState
+ return (r, c)
+ putCommonState newState
+ return result
+
+instance {-# OVERLAPPING #-} Exposable PandocError (PandocLua NumResults) where
+ partialApply _narg = liftLua . unPandocLua
+
+instance Pushable a => Exposable PandocError (PandocLua a) where
+ partialApply _narg x = 1 <$ (liftLua (unPandocLua x >>= Lua.push))
+
+-- | Global variables which should always be set.
+defaultGlobals :: PandocMonad m => m [Global]
+defaultGlobals = do
+ commonState <- getCommonState
+ return
+ [ PANDOC_API_VERSION
+ , PANDOC_STATE commonState
+ , PANDOC_VERSION
+ ]
+
+instance MonadError PandocError PandocLua where
+ catchError = Catch.catch
+ throwError = Catch.throwM
+
+instance PandocMonad PandocLua where
+ lookupEnv = IO.lookupEnv
+ getCurrentTime = IO.getCurrentTime
+ getCurrentTimeZone = IO.getCurrentTimeZone
+ newStdGen = IO.newStdGen
+ newUniqueHash = IO.newUniqueHash
+
+ openURL = IO.openURL
+
+ readFileLazy = IO.readFileLazy
+ readFileStrict = IO.readFileStrict
+ readStdinStrict = IO.readStdinStrict
+
+ glob = IO.glob
+ fileExists = IO.fileExists
+ getDataFileName = IO.getDataFileName
+ getModificationTime = IO.getModificationTime
+
+ getCommonState = PandocLua $ do
+ Lua.getglobal "PANDOC_STATE"
+ forcePeek $ peekCommonState Lua.top
+ putCommonState = PandocLua . setGlobals . (:[]) . PANDOC_STATE
+
+ logOutput = IO.logOutput
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Reader.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Reader.hs
new file mode 100644
index 000000000..6303dace3
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Reader.hs
@@ -0,0 +1,84 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{- |
+ Module : Text.Pandoc.Lua.Reader
+ Copyright : Copyright (C) 2021-2022 John MacFarlane
+ License : GNU GPL, version 2 or above
+
+ Maintainer : John MacFarlane <[email protected]>
+ Stability : alpha
+ Portability : portable
+
+Supports custom parsers written in Lua which produce a Pandoc AST.
+-}
+module Text.Pandoc.Lua.Reader ( readCustom ) where
+import Control.Exception
+import Control.Monad (when)
+import Control.Monad.IO.Class (MonadIO)
+import Data.Maybe (fromMaybe)
+import HsLua as Lua hiding (Operation (Div))
+import Text.Pandoc.Definition
+import Text.Pandoc.Class (PandocMonad, findFileWithDataFallback, report)
+import Text.Pandoc.Logging
+import Text.Pandoc.Lua.Global (Global (..), setGlobals)
+import Text.Pandoc.Lua.Init (runLua)
+import Text.Pandoc.Lua.PandocLua
+import Text.Pandoc.Lua.Marshal.Pandoc (peekPandoc)
+import Text.Pandoc.Options
+import Text.Pandoc.Sources (ToSources(..), sourcesToText)
+import qualified Data.Text as T
+
+-- | Convert custom markup to Pandoc.
+readCustom :: (PandocMonad m, MonadIO m, ToSources s)
+ => FilePath -> ReaderOptions -> s -> m Pandoc
+readCustom luaFile opts srcs = do
+ let globals = [ PANDOC_SCRIPT_FILE luaFile ]
+ luaFile' <- fromMaybe luaFile <$> findFileWithDataFallback "readers" luaFile
+ res <- runLua $ do
+ setGlobals globals
+ stat <- dofileTrace luaFile'
+ -- check for error in lua script (later we'll change the return type
+ -- to handle this more gracefully):
+ when (stat /= Lua.OK)
+ Lua.throwErrorAsException
+ parseCustom
+ case res of
+ Left msg -> throw msg
+ Right doc -> return doc
+ where
+ parseCustom = do
+ let input = toSources srcs
+ getglobal "Reader"
+ push input
+ push opts
+ pcallTrace 2 1 >>= \case
+ OK -> forcePeek $ peekPandoc top
+ ErrRun -> do
+ -- Caught a runtime error. Check if parsing might work if we
+ -- pass a string instead of a Sources list, then retry.
+ runPeek (peekText top) >>= \case
+ Failure {} ->
+ -- not a string error object. Bail!
+ throwErrorAsException
+ Success errmsg -> do
+ if "string expected, got pandoc Sources" `T.isInfixOf` errmsg
+ then do
+ pop 1
+ _ <- unPandocLua $ do
+ report $ Deprecated "old Reader function signature" $
+ T.unlines
+ [ "Reader functions should accept a sources list; "
+ , "functions expecting `string` input are deprecated. "
+ , "Use `tostring` to convert the first argument to a "
+ , "string."
+ ]
+ getglobal "Reader"
+ push $ sourcesToText input -- push sources as string
+ push opts
+ callTrace 2 1
+ forcePeek $ peekPandoc top
+ else
+ -- nothing we can do here
+ throwErrorAsException
+ _ -> -- not a runtime error, we won't be able to recover from that
+ throwErrorAsException
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Writer.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Writer.hs
new file mode 100644
index 000000000..f216ea63b
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Writer.hs
@@ -0,0 +1,63 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{- |
+ Module : Text.Pandoc.Lua.Writer
+ Copyright : Copyright (C) 2012-2022 John MacFarlane
+ License : GNU GPL, version 2 or above
+
+ Maintainer : John MacFarlane <[email protected]>
+ Stability : alpha
+ Portability : portable
+
+Conversion of Pandoc documents using a custom Lua writer.
+-}
+module Text.Pandoc.Lua.Writer
+ ( writeCustom
+ ) where
+
+import Control.Exception
+import Control.Monad ((<=<))
+import Data.Maybe (fromMaybe)
+import Data.Text (Text)
+import HsLua
+import Control.Monad.IO.Class (MonadIO)
+import Text.Pandoc.Class (PandocMonad, findFileWithDataFallback)
+import Text.Pandoc.Definition (Pandoc (..))
+import Text.Pandoc.Lua.Global (Global (..), setGlobals)
+import Text.Pandoc.Lua.Init (runLua)
+import Text.Pandoc.Options (WriterOptions)
+import qualified Text.Pandoc.Lua.Writer.Classic as Classic
+
+-- | Convert Pandoc to custom markup.
+writeCustom :: (PandocMonad m, MonadIO m)
+ => FilePath -> WriterOptions -> Pandoc -> m Text
+writeCustom luaFile opts doc = do
+ luaFile' <- fromMaybe luaFile <$> findFileWithDataFallback "writers" luaFile
+ either throw pure <=< runLua $ do
+ setGlobals [ PANDOC_DOCUMENT doc
+ , PANDOC_SCRIPT_FILE luaFile'
+ , PANDOC_WRITER_OPTIONS opts
+ ]
+ dofileTrace luaFile' >>= \case
+ OK -> pure ()
+ _ -> throwErrorAsException
+ -- Most classic writers contain code that throws an error if a global
+ -- is not present. This would break our check for the existence of a
+ -- "Writer" function. We resort to raw access for that reason, but
+ -- could also catch the error instead.
+ let rawgetglobal x = do
+ pushglobaltable
+ pushName x
+ rawget (nth 2) <* remove (nth 2) -- remove global table
+
+ rawgetglobal "Writer" >>= \case
+ TypeNil -> do
+ pop 1 -- remove nil
+ Classic.runCustom opts doc
+ _ -> do
+ -- Writer on top of the stack. Call it with document and writer
+ -- options as arguments.
+ push doc
+ push opts
+ callTrace 2 1
+ forcePeek $ peekText top
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Writer/Classic.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Writer/Classic.hs
new file mode 100644
index 000000000..522bdb651
--- /dev/null
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Writer/Classic.hs
@@ -0,0 +1,250 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+{- |
+ Module : Text.Pandoc.Lua.Writer.Classic
+ Copyright : Copyright (C) 2012-2022 John MacFarlane
+ License : GNU GPL, version 2 or above
+
+ Maintainer : John MacFarlane <[email protected]>
+ Stability : alpha
+ Portability : portable
+
+Conversion of Pandoc documents using a \"classic\" custom Lua writer.
+-}
+module Text.Pandoc.Lua.Writer.Classic
+ ( runCustom
+ ) where
+import Control.Applicative (optional)
+import Control.Arrow ((***))
+import Data.List (intersperse)
+import Data.Maybe (fromMaybe)
+import qualified Data.Text as T
+import Data.Text (Text, pack)
+import HsLua as Lua hiding (Operation (Div))
+#if !MIN_VERSION_hslua(2,2,0)
+import HsLua.Aeson (peekViaJSON)
+#endif
+import Text.DocLayout (literal, render)
+import Text.DocTemplates (Context)
+import Text.Pandoc.Definition
+import Text.Pandoc.Lua.Marshal.Attr (pushAttributeList)
+import Text.Pandoc.Lua.Orphans ()
+import Text.Pandoc.Options
+import Text.Pandoc.Templates (renderTemplate)
+import Text.Pandoc.Writers.Shared
+
+-- | List of key-value pairs that is pushed to Lua as AttributeList
+-- userdata.
+newtype AttributeList = AttributeList [(Text, Text)]
+instance Pushable AttributeList where
+ push (AttributeList kvs) = pushAttributeList kvs
+
+attrToMap :: Attr -> AttributeList
+attrToMap (id',classes,keyvals) = AttributeList
+ $ ("id", id')
+ : ("class", T.unwords classes)
+ : keyvals
+
+newtype Stringify a = Stringify a
+
+instance Pushable (Stringify Format) where
+ push (Stringify (Format f)) = Lua.push (T.toLower f)
+
+instance Pushable (Stringify [Inline]) where
+ push (Stringify ils) = Lua.push =<< inlineListToCustom ils
+
+instance Pushable (Stringify [Block]) where
+ push (Stringify blks) = Lua.push =<< blockListToCustom blks
+
+instance Pushable (Stringify MetaValue) where
+ push (Stringify (MetaMap m)) = Lua.push (fmap Stringify m)
+ push (Stringify (MetaList xs)) = Lua.push (map Stringify xs)
+ push (Stringify (MetaBool x)) = Lua.push x
+ push (Stringify (MetaString s)) = Lua.push s
+ push (Stringify (MetaInlines ils)) = Lua.push (Stringify ils)
+ push (Stringify (MetaBlocks bs)) = Lua.push (Stringify bs)
+
+instance Pushable (Stringify Citation) where
+ push (Stringify cit) = flip pushAsTable cit
+ [ ("citationId", push . citationId)
+ , ("citationPrefix", push . Stringify . citationPrefix)
+ , ("citationSuffix", push . Stringify . citationSuffix)
+ , ("citationMode", push . citationMode)
+ , ("citationNoteNum", push . citationNoteNum)
+ , ("citationHash", push . citationHash)
+ ]
+
+-- | Key-value pair, pushed as a table with @a@ as the only key and @v@ as the
+-- associated value.
+newtype KeyValue a b = KeyValue (a, b)
+
+instance (Pushable a, Pushable b) => Pushable (KeyValue a b) where
+ push (KeyValue (k, v)) = do
+ Lua.newtable
+ Lua.push k
+ Lua.push v
+ Lua.rawset (Lua.nth 3)
+
+-- | Convert Pandoc to custom markup using a classic Lua writer.
+runCustom :: LuaError e
+ => WriterOptions
+ -> Pandoc
+ -> LuaE e Text
+runCustom opts doc@(Pandoc meta _) = do
+ (body, context) <- docToCustom opts doc
+ -- convert metavalues to a template context (variables)
+ metaContext <- metaToContext opts
+ (fmap (literal . pack) . blockListToCustom)
+ (fmap (literal . pack) . inlineListToCustom)
+ meta
+ -- merge contexts from metadata and variables
+ let renderContext = context <> metaContext
+ return $ case writerTemplate opts of
+ Nothing -> body
+ Just tpl -> render Nothing $
+ renderTemplate tpl $ setField "body" body renderContext
+
+-- | Converts a Pandoc value to custom markup using a classic Lua writer.
+docToCustom :: forall e. LuaError e
+ => WriterOptions -> Pandoc -> LuaE e (Text, Context Text)
+docToCustom opts (Pandoc (Meta metamap) blocks) = do
+ body <- blockListToCustom blocks
+ -- invoke doesn't work with multiple return values, so we have to call
+ -- `Doc` manually.
+ Lua.getglobal "Doc" -- function
+ push body -- argument 1
+ push (fmap Stringify metamap) -- argument 2
+ push (writerVariables opts) -- argument 3
+ call 3 2
+ rendered <- peek (nth 2) -- first return value
+ context <- forcePeek . optional $ peekViaJSON top -- snd return value
+ return (rendered, fromMaybe mempty context)
+
+
+-- | Convert Pandoc block element to Custom.
+blockToCustom :: forall e. LuaError e
+ => Block -- ^ Block element
+ -> LuaE e String
+
+blockToCustom Null = return ""
+
+blockToCustom (Plain inlines) = invoke "Plain" (Stringify inlines)
+
+blockToCustom (Para [Image attr txt (src,tit)]) =
+ invoke "CaptionedImage" src tit (Stringify txt) (attrToMap attr)
+
+blockToCustom (Para inlines) = invoke "Para" (Stringify inlines)
+
+blockToCustom (LineBlock linesList) =
+ invoke "LineBlock" (map (Stringify) linesList)
+
+blockToCustom (RawBlock format str) =
+ invoke "RawBlock" (Stringify format) str
+
+blockToCustom HorizontalRule = invoke "HorizontalRule"
+
+blockToCustom (Header level attr inlines) =
+ invoke "Header" level (Stringify inlines) (attrToMap attr)
+
+blockToCustom (CodeBlock attr str) =
+ invoke "CodeBlock" str (attrToMap attr)
+
+blockToCustom (BlockQuote blocks) =
+ invoke "BlockQuote" (Stringify blocks)
+
+blockToCustom (Table _ blkCapt specs thead tbody tfoot) =
+ let (capt, aligns, widths, headers, rows) = toLegacyTable blkCapt specs thead tbody tfoot
+ aligns' = map show aligns
+ capt' = Stringify capt
+ headers' = map (Stringify) headers
+ rows' = map (map (Stringify)) rows
+ in invoke "Table" capt' aligns' widths headers' rows'
+
+blockToCustom (BulletList items) =
+ invoke "BulletList" (map (Stringify) items)
+
+blockToCustom (OrderedList (num,sty,delim) items) =
+ invoke "OrderedList" (map (Stringify) items) num (show sty) (show delim)
+
+blockToCustom (DefinitionList items) =
+ invoke "DefinitionList"
+ (map (KeyValue . (Stringify *** map (Stringify))) items)
+
+blockToCustom (Div attr items) =
+ invoke "Div" (Stringify items) (attrToMap attr)
+
+-- | Convert list of Pandoc block elements to Custom.
+blockListToCustom :: forall e. LuaError e
+ => [Block] -- ^ List of block elements
+ -> LuaE e String
+blockListToCustom xs = do
+ blocksep <- invoke "Blocksep"
+ bs <- mapM blockToCustom xs
+ return $ mconcat $ intersperse blocksep bs
+
+-- | Convert list of Pandoc inline elements to Custom.
+inlineListToCustom :: forall e. LuaError e => [Inline] -> LuaE e String
+inlineListToCustom lst = do
+ xs <- mapM (inlineToCustom @e) lst
+ return $ mconcat xs
+
+-- | Convert Pandoc inline element to Custom.
+inlineToCustom :: forall e. LuaError e => Inline -> LuaE e String
+
+inlineToCustom (Str str) = invoke "Str" str
+
+inlineToCustom Space = invoke "Space"
+
+inlineToCustom SoftBreak = invoke "SoftBreak"
+
+inlineToCustom (Emph lst) = invoke "Emph" (Stringify lst)
+
+inlineToCustom (Underline lst) = invoke "Underline" (Stringify lst)
+
+inlineToCustom (Strong lst) = invoke "Strong" (Stringify lst)
+
+inlineToCustom (Strikeout lst) = invoke "Strikeout" (Stringify lst)
+
+inlineToCustom (Superscript lst) = invoke "Superscript" (Stringify lst)
+
+inlineToCustom (Subscript lst) = invoke "Subscript" (Stringify lst)
+
+inlineToCustom (SmallCaps lst) = invoke "SmallCaps" (Stringify lst)
+
+inlineToCustom (Quoted SingleQuote lst) =
+ invoke "SingleQuoted" (Stringify lst)
+
+inlineToCustom (Quoted DoubleQuote lst) =
+ invoke "DoubleQuoted" (Stringify lst)
+
+inlineToCustom (Cite cs lst) =
+ invoke "Cite" (Stringify lst) (map (Stringify) cs)
+
+inlineToCustom (Code attr str) =
+ invoke "Code" str (attrToMap attr)
+
+inlineToCustom (Math DisplayMath str) =
+ invoke "DisplayMath" str
+
+inlineToCustom (Math InlineMath str) =
+ invoke "InlineMath" str
+
+inlineToCustom (RawInline format str) =
+ invoke "RawInline" (Stringify format) str
+
+inlineToCustom LineBreak = invoke "LineBreak"
+
+inlineToCustom (Link attr txt (src,tit)) =
+ invoke "Link" (Stringify txt) src tit (attrToMap attr)
+
+inlineToCustom (Image attr alt (src,tit)) =
+ invoke "Image" (Stringify alt) src tit (attrToMap attr)
+
+inlineToCustom (Note contents) = invoke "Note" (Stringify contents)
+
+inlineToCustom (Span attr items) =
+ invoke "Span" (Stringify items) (attrToMap attr)