diff options
| author | Albert Krewinkel <[email protected]> | 2022-09-29 17:24:31 +0200 |
|---|---|---|
| committer | John MacFarlane <[email protected]> | 2022-09-30 08:33:40 -0700 |
| commit | 5be9052f5fb7283372b3d5497bef499718a34992 (patch) | |
| tree | 80e5805786ef7ab08f363135861e1aa9c8868f6f /pandoc-lua-engine/src | |
| parent | 79980eee4a1854921d7fb8b14848894b53cc21a7 (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')
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) |
