pandoc-lua-engine 0.2.1.5 → 0.3
raw patch · 16 files changed
+601/−315 lines, 16 filesdep ~pandoc
Dependency ranges changed: pandoc
Files
- pandoc-lua-engine.cabal +6/−3
- src/Text/Pandoc/Lua.hs +3/−23
- src/Text/Pandoc/Lua/Custom.hs +1/−1
- src/Text/Pandoc/Lua/Engine.hs +73/−0
- src/Text/Pandoc/Lua/Filter.hs +35/−49
- src/Text/Pandoc/Lua/Init.hs +33/−218
- src/Text/Pandoc/Lua/Marshal/LogMessage.hs +1/−1
- src/Text/Pandoc/Lua/Module.hs +160/−0
- src/Text/Pandoc/Lua/Module/Template.hs +44/−13
- src/Text/Pandoc/Lua/Module/Utils.hs +35/−0
- src/Text/Pandoc/Lua/PandocLua.hs +3/−1
- src/Text/Pandoc/Lua/Run.hs +82/−0
- test/lua/module/globals.lua +47/−1
- test/lua/module/pandoc-json.lua +15/−4
- test/lua/module/pandoc-template.lua +19/−1
- test/lua/module/pandoc-utils.lua +44/−0
pandoc-lua-engine.cabal view
@@ -1,10 +1,10 @@ cabal-version: 2.4 name: pandoc-lua-engine-version: 0.2.1.5+version: 0.3 build-type: Simple license: GPL-2.0-or-later license-file: COPYING.md-copyright: © 2006-2022 John MacFarlane, 2017-2022 Albert Krewinkel+copyright: © 2006-2024 John MacFarlane, 2017-2024 Albert Krewinkel author: John MacFarlane, Albert Krewinkel maintainer: Albert Krewinkel <albert@hslua.org> bug-reports: https://github.com/jgm/pandoc/issues@@ -67,9 +67,11 @@ hs-source-dirs: src exposed-modules: Text.Pandoc.Lua other-modules: Text.Pandoc.Lua.Custom+ , Text.Pandoc.Lua.Engine , Text.Pandoc.Lua.Filter , Text.Pandoc.Lua.Global , Text.Pandoc.Lua.Init+ , Text.Pandoc.Lua.Module , Text.Pandoc.Lua.Marshal.Chunks , Text.Pandoc.Lua.Marshal.CommonState , Text.Pandoc.Lua.Marshal.Context@@ -98,6 +100,7 @@ , Text.Pandoc.Lua.Module.Utils , Text.Pandoc.Lua.Orphans , Text.Pandoc.Lua.PandocLua+ , Text.Pandoc.Lua.Run , Text.Pandoc.Lua.SourcePos , Text.Pandoc.Lua.Writer.Classic , Text.Pandoc.Lua.Writer.Scaffolding@@ -121,7 +124,7 @@ , hslua-repl >= 0.1.1 && < 0.2 , lpeg >= 1.1 && < 1.2 , mtl >= 2.2 && < 2.4- , pandoc >= 3.1.4 && < 3.3+ , pandoc >= 3.2.1 && < 3.3 , pandoc-lua-marshal >= 0.2.7 && < 0.3 , pandoc-types >= 1.22 && < 1.24 , parsec >= 3.1 && < 3.2
src/Text/Pandoc/Lua.hs view
@@ -1,5 +1,3 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TypeApplications #-} {- | Module : Text.Pandoc.Lua Copyright : Copyright © 2017-2024 Albert Krewinkel@@ -23,26 +21,8 @@ , 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.Custom (loadCustom)+import Text.Pandoc.Lua.Engine (getEngine, applyFilter)+import Text.Pandoc.Lua.Global (Global (..), setGlobals)+import Text.Pandoc.Lua.Run (runLua, runLuaNoEnv) 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- , engineLoadCustom = loadCustom- }
src/Text/Pandoc/Lua/Custom.hs view
@@ -18,11 +18,11 @@ import Text.Pandoc.Class (PandocMonad, findFileWithDataFallback) import Text.Pandoc.Error (PandocError) import Text.Pandoc.Lua.Global (Global (..), setGlobals)-import Text.Pandoc.Lua.Init (runLuaWith) import Text.Pandoc.Lua.Marshal.Format (peekExtensionsConfig) import Text.Pandoc.Lua.Marshal.Pandoc (peekPandoc) import Text.Pandoc.Lua.Marshal.WriterOptions (pushWriterOptions) import Text.Pandoc.Lua.PandocLua (unPandocLua)+import Text.Pandoc.Lua.Run (runLuaWith) import Text.Pandoc.Readers (Reader (..)) import Text.Pandoc.Sources (ToSources(..)) import Text.Pandoc.Scripting (CustomComponents (..))
+ src/Text/Pandoc/Lua/Engine.hs view
@@ -0,0 +1,73 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}+{- |+ Module : Text.Pandoc.Lua.Engine+ Copyright : Copyright © 2017-2024 Albert Krewinkel+ License : GPL-2.0-or-later+ Maintainer : Albert Krewinkel <tarleb+pandoc@zeitkraut.de>++Running pandoc Lua filters.+-}+module Text.Pandoc.Lua.Engine+ ( getEngine+ , applyFilter+ ) where++import Control.Exception (throw)+import Control.Monad ((>=>))+import Control.Monad.IO.Class (MonadIO (liftIO))+import HsLua.Core (getglobal, openlibs, run, top, tostring)+import Text.Pandoc.Class (PandocMonad)+import Text.Pandoc.Definition (Pandoc)+import Text.Pandoc.Filter (Environment (..))+import Text.Pandoc.Error (PandocError (PandocFilterError, PandocLuaError))+import Text.Pandoc.Lua.Custom (loadCustom)+import Text.Pandoc.Lua.Filter (runFilterFile)+import Text.Pandoc.Lua.Global (Global (..), setGlobals)+import Text.Pandoc.Lua.Run (runLua)+import Text.Pandoc.Lua.Orphans ()+import Text.Pandoc.Scripting (ScriptingEngine (..))+import qualified Text.Pandoc.UTF8 as UTF8+import qualified Data.Text as T++-- | 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+ , engineLoadCustom = loadCustom+ }++-- | 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
src/Text/Pandoc/Lua/Filter.hs view
@@ -1,41 +1,41 @@-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE IncoherentInstances #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-} {- | Module : Text.Pandoc.Lua.Filter Copyright : © 2012-2024 John MacFarlane, © 2017-2024 Albert Krewinkel-License : GNU GPL, version 2 or above-Maintainer : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>-Stability : alpha+License : GPL-2.0-or-later+Maintainer : Albert Krewinkel <pandoc@tarleb.com> Types and functions for running Lua filters. -} module Text.Pandoc.Lua.Filter- ( applyFilter+ ( runFilterFile+ , runFilterFile' ) where import Control.Monad ((>=>), (<$!>)) import HsLua as Lua-import Text.Pandoc.Definition-import Text.Pandoc.Filter (Environment (..))+import Text.Pandoc.Definition (Pandoc)+import Text.Pandoc.Error (PandocError) 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 Text.Pandoc.Lua.PandocLua ()-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.+-- Runs the filter in the global environment. runFilterFile :: FilePath -> Pandoc -> LuaE PandocError Pandoc runFilterFile filterPath doc = do+ Lua.pushglobaltable+ runFilterFile' Lua.top filterPath doc <* Lua.pop 1++-- | Like 'runFilterFile', but uses the table at the given index as the+-- environment in which the filter is run.+runFilterFile' :: StackIndex -> FilePath -> Pandoc+ -> LuaE PandocError Pandoc+runFilterFile' envIdx filterPath doc = do oldtop <- gettop- stat <- dofileTrace (Just filterPath)- if stat /= Lua.OK+ stat <- dofileTrace' envIdx (Just filterPath)+ if stat /= OK then throwErrorAsException else do newtop <- gettop@@ -43,39 +43,25 @@ -- filter if nothing was returned. luaFilters <- forcePeek $ if newtop - oldtop >= 1- then peekList peekFilter top- else (:[]) <$!> (liftLua pushglobaltable *> peekFilter top)+ then peekList peekFilter top -- get from explicit filter table+ else (:[]) <$!> peekFilter envIdx -- get the implicit filter in _ENV settop oldtop runAll luaFilters doc +-- | Apply Lua filters to a document 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+-- | Like 'HsLua.Core.Trace.dofileTrace', but uses a local environment.+dofileTrace' :: LuaError e+ => StackIndex -- ^ stack index of the environment table+ -> Maybe FilePath -- ^ file to load (or @Nothing@ for stdin)+ -> LuaE e Status+dofileTrace' envIdx fp = do+ absEnv <- Lua.absindex envIdx+ loadfile fp >>= \case+ OK -> do+ Lua.pushvalue absEnv+ Just (Name "_ENV") <- Lua.setupvalue (Lua.nth 2) 1+ pcallTrace 0 multret+ s -> pure s
src/Text/Pandoc/Lua/Init.hs view
@@ -2,244 +2,59 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {- |- Module : Text.Pandoc.Lua- Copyright : Copyright © 2017-2024 Albert Krewinkel- License : GNU GPL, version 2 or above-+ Module : Text.Pandoc.Lua.Init+ Copyright : © 2017-2024 Albert Krewinkel+ License : GPL-2.0-or-later Maintainer : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>- Stability : alpha Functions to initialize the Lua interpreter. -} module Text.Pandoc.Lua.Init- ( runLua- , runLuaNoEnv- , runLuaWith+ ( initLua+ , userInit ) where -import Control.Monad (forM, forM_, when)-import Control.Monad.Catch (throwM, try)-import Control.Monad.Trans (MonadIO (..))-import Data.Maybe (catMaybes)-import Data.Version (makeVersion)-import HsLua as Lua hiding (status, try)-import Text.Pandoc.Class (PandocMonad (..), report)+import Control.Monad (when)+import Control.Monad.Catch (throwM)+import HsLua as Lua hiding (status)+import Text.Pandoc.Class (report) import Text.Pandoc.Data (readDataFile) import Text.Pandoc.Error (PandocError (PandocLuaError)) import Text.Pandoc.Logging (LogMessage (ScriptingWarning))-import Text.Pandoc.Lua.Global (Global (..), setGlobals)-import Text.Pandoc.Lua.Marshal.List (newListMetatable, pushListModule)+import Text.Pandoc.Lua.Module (initModules) import Text.Pandoc.Lua.PandocLua (PandocLua (..), liftPandocLua) import Text.Pandoc.Lua.SourcePos (luaSourcePos)-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.Zip as Module.Zip-import qualified Text.Pandoc.Lua.Module.CLI as Pandoc.CLI-import qualified Text.Pandoc.Lua.Module.Format as Pandoc.Format-import qualified Text.Pandoc.Lua.Module.Image as Pandoc.Image-import qualified Text.Pandoc.Lua.Module.JSON as Pandoc.JSON-import qualified Text.Pandoc.Lua.Module.Log as Pandoc.Log-import qualified Text.Pandoc.Lua.Module.MediaBag as Pandoc.MediaBag-import qualified Text.Pandoc.Lua.Module.Pandoc as Module.Pandoc-import qualified Text.Pandoc.Lua.Module.Scaffolding as Pandoc.Scaffolding-import qualified Text.Pandoc.Lua.Module.Structure as Pandoc.Structure-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.Text as Pandoc.Text-import qualified Text.Pandoc.Lua.Module.Types as Pandoc.Types-import qualified Text.Pandoc.Lua.Module.Utils as Pandoc.Utils import qualified Text.Pandoc.UTF8 as UTF8 --- | 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 = do- runPandocLuaWith Lua.run . try $ do- initLuaState- liftPandocLua action--runLuaWith :: (PandocMonad m, MonadIO m)- => GCManagedState -> LuaE PandocError a -> m (Either PandocError a)-runLuaWith luaState action = do- runPandocLuaWith (withGCManagedState luaState) . 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 = do- runPandocLuaWith Lua.run . 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.CLI.documentedModule- , Pandoc.Format.documentedModule- , Pandoc.Image.documentedModule- , Pandoc.JSON.documentedModule- , Pandoc.Log.documentedModule- , Pandoc.MediaBag.documentedModule- , Pandoc.Scaffolding.documentedModule- , Pandoc.Structure.documentedModule- , Pandoc.System.documentedModule- , Pandoc.Template.documentedModule- , Pandoc.Text.documentedModule- , Pandoc.Types.documentedModule- , Pandoc.Utils.documentedModule- , Module.Layout.documentedModule { moduleName = "pandoc.layout" }- `allSince` [2,18]- , Module.Path.documentedModule { moduleName = "pandoc.path" }- `allSince` [2,12]- , Module.Zip.documentedModule { moduleName = "pandoc.zip" }- `allSince` [3,0]- ]- where- allSince mdl version = mdl- { moduleFunctions = map (`since` makeVersion version) $ moduleFunctions mdl- }---- | Initialize the lua state with all required values-initLuaState :: PandocLua ()-initLuaState = do+-- | Initialize Lua with all default and pandoc-specific libraries and default+-- globals.+initLua :: PandocLua ()+initLua = do liftPandocLua Lua.openlibs setWarnFunction- 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- -- pandoc.text must be require-able as 'text' for backwards compat.- when (moduleName mdl == "pandoc.text") $ do- getfield registryindex loaded- pushvalue (nth 2)- setfield (nth 2) "text"- pop 1 -- _LOADED- -- Shorten name, drop everything before the first dot (if any).- 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+ initModules+ liftPandocLua userInit --- | 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 ())- Lua.pop 1+-- | User-controlled initialization, e.g., running the user's init script.+userInit :: LuaE PandocError ()+userInit = runInitScript --- | Evaluate a @'PandocLua'@ computation, running all contained Lua--- operations.-runPandocLuaWith :: (PandocMonad m, MonadIO m)- => (forall b. LuaE PandocError b -> IO b)- -> PandocLua a- -> m a-runPandocLuaWith runner pLua = do- origState <- getCommonState- globals <- defaultGlobals- (result, newState) <- liftIO . runner . unPandocLua $ do- putCommonState origState- liftPandocLua $ setGlobals globals- r <- pLua- c <- getCommonState- return (r, c)- putCommonState newState- return result+-- | Run the @init.lua@ data file as a Lua script.+runInitScript :: LuaE PandocError ()+runInitScript = runDataFileScript "init.lua" --- | 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- ]+-- | Get a data file and run it as a Lua script.+runDataFileScript :: FilePath -> LuaE PandocError ()+runDataFileScript scriptFile = do+ script <- unPandocLua $ readDataFile scriptFile+ status <- Lua.dostring script+ when (status /= Lua.OK) $ 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 setWarnFunction :: PandocLua () setWarnFunction = liftPandocLua . setwarnf' $ \msg -> do
src/Text/Pandoc/Lua/Marshal/LogMessage.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE OverloadedStrings #-} {- | Module : Text.Pandoc.Lua.Marshal.LogMessage- Copyright : © 2017-2023 Albert Krewinkel+ Copyright : © 2017-2024 Albert Krewinkel License : GPL-2.0-or-later Maintainer : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>
+ src/Text/Pandoc/Lua/Module.hs view
@@ -0,0 +1,160 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{- |+ Module : Text.Pandoc.Lua.Module+ Copyright : © 2017-2024 Albert Krewinkel+ License : GPL-2.0-or-later+ Maintainer : Albert Krewinkel <pandoc@tarleb.com>++Setting up and initializing Lua modules.+-}++module Text.Pandoc.Lua.Module+ ( initModules+ ) where++import Control.Monad (forM, forM_, when)+import Data.Maybe (catMaybes)+import Data.Version (makeVersion)+import HsLua as Lua+import Text.Pandoc.Error (PandocError)+import Text.Pandoc.Lua.Marshal.List (pushPandocList, pushListModule)+import Text.Pandoc.Lua.PandocLua (PandocLua (..), liftPandocLua)+import qualified Data.ByteString.Char8 as Char8+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.Zip as Module.Zip+import qualified Text.Pandoc.Lua.Module.CLI as Pandoc.CLI+import qualified Text.Pandoc.Lua.Module.Format as Pandoc.Format+import qualified Text.Pandoc.Lua.Module.Image as Pandoc.Image+import qualified Text.Pandoc.Lua.Module.JSON as Pandoc.JSON+import qualified Text.Pandoc.Lua.Module.Log as Pandoc.Log+import qualified Text.Pandoc.Lua.Module.MediaBag as Pandoc.MediaBag+import qualified Text.Pandoc.Lua.Module.Pandoc as Module.Pandoc+import qualified Text.Pandoc.Lua.Module.Scaffolding as Pandoc.Scaffolding+import qualified Text.Pandoc.Lua.Module.Structure as Pandoc.Structure+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.Text as Pandoc.Text+import qualified Text.Pandoc.Lua.Module.Types as Pandoc.Types+import qualified Text.Pandoc.Lua.Module.Utils as Pandoc.Utils++initModules :: PandocLua ()+initModules = do+ initPandocModule+ initJsonMetatable+ installLpegSearcher+ setGlobalModules++initPandocModule :: PandocLua ()+initPandocModule = liftPandocLua $ do+ -- Push module table+ registerModule Module.Pandoc.documentedModule+ -- load modules and add them to the `pandoc` module table.+ forM_ submodules $ \mdl -> do+ registerModule mdl+ -- pandoc.text must be require-able as 'text' for backwards compat.+ when (moduleName mdl == "pandoc.text") $ do+ getfield registryindex loaded+ pushvalue (nth 2)+ setfield (nth 2) "text"+ pop 1 -- _LOADED+ -- Shorten name, drop everything before the first dot (if any).+ 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"++-- | 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.+submodules :: [Module PandocError]+submodules =+ [ Pandoc.CLI.documentedModule+ , Pandoc.Format.documentedModule+ , Pandoc.Image.documentedModule+ , Pandoc.JSON.documentedModule+ , Pandoc.Log.documentedModule+ , Pandoc.MediaBag.documentedModule+ , Pandoc.Scaffolding.documentedModule+ , Pandoc.Structure.documentedModule+ , Pandoc.System.documentedModule+ , Pandoc.Template.documentedModule+ , Pandoc.Text.documentedModule+ , Pandoc.Types.documentedModule+ , Pandoc.Utils.documentedModule+ , Module.Layout.documentedModule { moduleName = "pandoc.layout" }+ `allSince` [2,18]+ , Module.Path.documentedModule { moduleName = "pandoc.path" }+ `allSince` [2,12]+ , Module.Zip.documentedModule { moduleName = "pandoc.zip" }+ `allSince` [3,0]+ ]+ where+ allSince mdl version = mdl+ { moduleFunctions = map (`since` makeVersion version) $ moduleFunctions mdl+ }++-- | Load all global modules and set them to their global variables.+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+ pushPandocList (const pushnil) []+ getmetatable top+ setfield registryindex HsLua.Aeson.jsonarray+ Lua.pop 1
src/Text/Pandoc/Lua/Module/Template.hs view
@@ -21,7 +21,7 @@ import Text.Pandoc.Lua.PandocLua (PandocLua (unPandocLua), liftPandocLua) import Text.Pandoc.Writers.Shared (metaToContext') import Text.Pandoc.Templates- ( compileTemplate, getDefaultTemplate, renderTemplate+ ( compileTemplate, getDefaultTemplate, getTemplate, renderTemplate , runWithPartials, runWithDefaultPartials ) import qualified Data.Text as T@@ -31,7 +31,7 @@ documentedModule = Module { moduleName = "pandoc.template" , moduleDescription = T.unlines- [ "Lua functions for pandoc templates."+ [ "Handle pandoc templates." ] , moduleFields = [] , moduleOperations = []@@ -50,7 +50,7 @@ #? T.unlines [ "Applies a context with variable assignments to a template," , "returning the rendered template. The `context` parameter must be a"- , "table with variable names as keys and [Doc], string, boolean, or"+ , "table with variable names as keys and [[Doc]], string, boolean, or" , "table as values, where the table can be either be a list of the" , "aforementioned types, or a nested context." ]@@ -63,9 +63,22 @@ Nothing -> runWithDefaultPartials (compileTemplate "templates/default" template)) <#> parameter peekText "string" "template" "template string"- <#> opt (stringParam "templ_path" "template path")- =#> functionResult (either failLua pushTemplate) "pandoc Template"+ <#> opt (stringParam "templates_path"+ ("parameter to determine a default path and extension for " <>+ "partials; uses the data files templates path by default."))+ =#> functionResult (either failLua pushTemplate) "Template" "compiled template"+ #? T.unlines+ [ "Compiles a template string into a [[Template]] object usable by"+ , "pandoc."+ , ""+ , "If the `templates_path` parameter is specified, then it should be the"+ , "file path associated with the template. It is used when checking"+ , "for partials. Partials will be taken only from the default data"+ , "files if this parameter is omitted."+ , ""+ , "An error is raised if compilation fails."+ ] `since` makeVersion [2,17] , defun "default"@@ -76,11 +89,29 @@ 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"+ ("name of the writer for which the template should be " <>+ "retrieved; defaults to the global `FORMAT`."))+ =#> functionResult pushText "string" "raw template"+ #? T.unlines+ [ "Returns the default template for a given writer as a string. An"+ , "error is thrown if no such template can be found."+ ] `since` makeVersion [2,17] + , defun "get"+ ### (unPandocLua . getTemplate)+ <#> stringParam "filename" "name of the template"+ =#> textResult "content of template file"+ #? T.unlines+ [ "Retrieve text for a template."+ , ""+ , "This function first checks the resource paths for a file of this"+ , "name; if none is found, the `templates` directory in the user data"+ , "directory is checked. Returns the content of the file, or throws"+ , "an error if no file is found."+ ]+ `since` makeVersion [3,2,1]+ , defun "meta_to_context" ### (\meta blockWriterIdx inlineWriterIdx -> unPandocLua $ do let blockWriter blks = liftPandocLua $ do@@ -96,14 +127,14 @@ metaToContext' blockWriter inlineWriter meta) <#> parameter peekMeta "Meta" "meta" "document metadata" <#> parameter pure "function" "blocks_writer"- "converter from Blocks to Doc values"+ "converter from [[Blocks]] to [[Doc]] values" <#> parameter pure "function" "inlines_writer"- "converter from Inlines to Doc values"+ "converter from [[Inlines]] to [[Doc]] values" =#> functionResult pushContext "table" "template context" #? T.unlines- [ "Creates template context from the document's [Meta]{#type-meta}"- , "data, using the given functions to convert [Blocks] and [Inlines]"- , "to [Doc] values."+ [ "Creates template context from the document's [[Meta]] data, using the"+ , "given functions to convert [[Blocks]] and [[Inlines]] to [[Doc]]"+ , "values." ] `since` makeVersion [3,0] ]
src/Text/Pandoc/Lua/Module/Utils.hs view
@@ -29,6 +29,7 @@ import Text.Pandoc.Definition import Text.Pandoc.Error (PandocError) import Text.Pandoc.Filter (applyJSONFilter)+import Text.Pandoc.Lua.Filter (runFilterFile') import Text.Pandoc.Lua.Marshal.AST import Text.Pandoc.Lua.Marshal.Reference import Text.Pandoc.Lua.PandocLua (PandocLua (unPandocLua))@@ -60,6 +61,7 @@ , from_simple_table `since` v[2,11] , make_sections `since` v[2,8] , references `since` v[2,17]+ , run_lua_filter `since` v[3,2,1] , run_json_filter `since` v[2,1,1] , normalize_date `since` v[2,0,6] , sha1 `since` v[2,0,6]@@ -246,6 +248,39 @@ , " end" ] +-- | Run a filter from a file.+run_lua_filter :: DocumentedFunction PandocError+run_lua_filter = defun "run_lua_filter"+ ### (\doc fp mbenv -> do+ envIdx <- maybe copyOfGlobalTable pure mbenv+ runFilterFile' envIdx fp doc)+ <#> parameter peekPandoc "Pandoc" "doc" "the Pandoc document to filter"+ <#> parameter peekString "string" "filter" "filepath of the filter to run"+ <#> opt (parameter (typeChecked "table" istable pure) "table" "env"+ "environment to load and run the filter in")+ =#> functionResult pushPandoc "Pandoc" "filtered document"+ #? ( "Filter the given doc by passing it through a Lua filter." <>+ "\n\nThe filter will be run in the current Lua process." <>+ "\n"+ )+ where+ copynext :: LuaError e => StackIndex -> LuaE e StackIndex+ copynext to =+ Lua.next (nth 2) >>= \case+ False -> pure to+ True -> do+ pushvalue (nth 2)+ insert (nth 2)+ rawset to+ copynext to+ copyOfGlobalTable :: LuaError e => LuaE e StackIndex+ copyOfGlobalTable = do+ newtable+ pushglobaltable+ pushnil+ (copynext =<< absindex (nth 3)) <* pop 1 -- pop source table++-- | Process the document with a JSON filter. run_json_filter :: DocumentedFunction PandocError run_json_filter = defun "run_json_filter" ### (\doc filterPath margs -> do
src/Text/Pandoc/Lua/PandocLua.hs view
@@ -77,10 +77,12 @@ getModificationTime = IO.getModificationTime getCommonState = PandocLua $ do- Lua.getglobal "PANDOC_STATE"+ Lua.getfield registryindex "PANDOC_STATE" forcePeek $ peekCommonState Lua.top `lastly` pop 1 putCommonState cst = PandocLua $ do pushCommonState cst+ Lua.pushvalue Lua.top+ Lua.setfield registryindex "PANDOC_STATE" Lua.setglobal "PANDOC_STATE" logOutput = IO.logOutput
+ src/Text/Pandoc/Lua/Run.hs view
@@ -0,0 +1,82 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{- |+ Module : Text.Pandoc.Lua.Run+ Copyright : Copyright © 2017-2024 Albert Krewinkel+ License : GPL-2.0-or-later+ Maintainer : Albert Krewinkel <tarleb+pandoc@moltkeplatz.de>++Run code in the Lua interpreter.+-}+module Text.Pandoc.Lua.Run+ ( runLua+ , runLuaNoEnv+ , runLuaWith+ ) where++import Control.Monad.Catch (try)+import Control.Monad.Trans (MonadIO (..))+import HsLua as Lua hiding (try)+import Text.Pandoc.Class (PandocMonad (..))+import Text.Pandoc.Error (PandocError)+import Text.Pandoc.Lua.Global (Global (..), setGlobals)+import Text.Pandoc.Lua.Init (initLua)+import Text.Pandoc.Lua.PandocLua (PandocLua (..), liftPandocLua)++-- | 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 = do+ runPandocLuaWith Lua.run . try $ do+ initLua+ liftPandocLua action++runLuaWith :: (PandocMonad m, MonadIO m)+ => GCManagedState -> LuaE PandocError a -> m (Either PandocError a)+runLuaWith luaState action = do+ runPandocLuaWith (withGCManagedState luaState) . try $ do+ initLua+ 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 = do+ runPandocLuaWith Lua.run . 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"+ initLua+ liftPandocLua action++-- | Evaluate a @'PandocLua'@ computation, running all contained Lua+-- operations.+runPandocLuaWith :: (PandocMonad m, MonadIO m)+ => (forall b. LuaE PandocError b -> IO b)+ -> PandocLua a+ -> m a+runPandocLuaWith runner pLua = do+ origState <- getCommonState+ globals <- defaultGlobals+ (result, newState) <- liftIO . runner . unPandocLua $ do+ putCommonState origState+ liftPandocLua $ setGlobals globals+ r <- pLua+ c <- getCommonState+ return (r, c)+ putCommonState newState+ return result++-- | 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+ ]
test/lua/module/globals.lua view
@@ -107,5 +107,51 @@ test('wrap_text', function () assert.are_equal(type(PANDOC_WRITER_OPTIONS.wrap_text), 'string') end),- }+ },++ group 'PANDOC_STATE' {+ test('is a userdata object', function ()+ assert.are_equal(type(PANDOC_STATE), 'userdata')+ end),+ test('has property "input_files"', function ()+ assert.are_equal(type(PANDOC_STATE.input_files), 'table')+ end),+ test('has optional property "output_file"', function ()+ -- property may be nil+ if PANDOC_STATE.output_file then+ assert.are_equal(type(PANDOC_STATE.output_file), 'string')+ end+ end),+ test('has property "log"', function ()+ assert.are_equal(type(PANDOC_STATE.log), 'table')+ end),+ test('has property "request_headers"', function ()+ assert.are_equal(type(PANDOC_STATE.request_headers), 'table')+ end),+ test('has property "resource_path"', function ()+ assert.are_equal(type(PANDOC_STATE.resource_path), 'table')+ end),+ test('has optional property "source_url"', function ()+ if PANDOC_STATE.source_url then+ assert.are_equal(type(PANDOC_STATE.source_url), 'string')+ end+ end),+ test('has property "trace"', function ()+ assert.are_equal(type(PANDOC_STATE.trace), 'boolean')+ end),+ test('has optional property "user_data_dir"', function ()+ if PANDOC_STATE.user_data_dir then+ assert.are_equal(type(PANDOC_STATE.user_data_dir), 'string')+ end+ end),+ test('has property "verbosity"', function ()+ assert.are_equal(type(PANDOC_STATE.verbosity), 'string')+ end),+ test('can be deleted without breaking PandocLua monad functions', function()+ local state = PANDOC_STATE+ PANDOC_STATE = nil+ assert.is_nil(pandoc.mediabag.lookup('does-not-exist'))+ PANDOC_STATE = state+ end),+ }, }
test/lua/module/pandoc-json.lua view
@@ -1,6 +1,7 @@ -- -- Tests for the system module --+local pandoc = require 'pandoc' local json = require 'pandoc.json' local tasty = require 'tasty' @@ -33,19 +34,26 @@ local obj = setmetatable( {title = 23}, {- __tojson = function (obj)+ __tojson = function (_) return '"Nichts ist so wie es scheint"' end } ) assert.are_same(json.encode(obj), [["Nichts ist so wie es scheint"]]) end),- test('Inline (Space)', function ()+ test('pandoc.List', function ()+ local list = pandoc.List {'foo', 'bar', 'baz'} assert.are_equal(- json.encode(pandoc.Space()),- '{"t":"Space"}'+ json.encode(list),+ '["foo","bar","baz"]' ) end),+ test('Inline (Space)', function ()+ assert.are_equal(+ json.encode(pandoc.Space()),+ '{"t":"Space"}'+ )+ end), test('Block (HorizontalRule)', function () assert.are_equal( json.encode(pandoc.HorizontalRule()),@@ -84,6 +92,9 @@ end), test('object', function () assert.are_same(json.decode '{"a":5}', {a = 5})+ end),+ test('list of strings', function ()+ assert.are_equal(json.decode '["foo", "bar"]', pandoc.List{"foo", "bar"}) end), test('Inline (Space)', function () assert.are_equal(json.decode '{"t":"Space"}', pandoc.Space())
test/lua/module/pandoc-template.lua view
@@ -1,4 +1,5 @@ local tasty = require 'tasty'+local pandoc = require 'pandoc' local template = require 'pandoc.template' local assert = tasty.assert@@ -24,8 +25,25 @@ ) end), test('fails on unknown format', function ()+ local success, msg = pcall(function ()+ return pandoc.utils.type(template.default 'nosuchformat')+ end)+ assert.is_falsy(success)+ end),+ },+ group 'get' {+ test('is function', function ()+ assert.are_equal(type(template.get), 'function')+ end),+ test('searches the template data directory', function ()+ assert.are_equal(+ template.default 'html5',+ template.get 'default.html5'+ )+ end),+ test('fails on non-existent file', function () local success, msg = pcall(function ()- return pandoc.utils.type(template.default 'nosuchformat')+ return pandoc.utils.type(template.get 'nosuchfile.nope') end) assert.is_falsy(success) end),
test/lua/module/pandoc-utils.lua view
@@ -1,5 +1,7 @@ local tasty = require 'tasty'+local pandoc = require 'pandoc' local utils = require 'pandoc.utils'+local io = require 'io' local assert = tasty.assert local test = tasty.test_case@@ -124,6 +126,48 @@ ) local doc = pandoc.Pandoc({}, {nocite = nocite, references = {ref}}) assert.are_same({ref}, pandoc.utils.references(doc))+ end)+ },++ group 'run_lua_filter' {+ test('runs a filter', function ()+ local doc = pandoc.Pandoc("indivisible words")+ pandoc.system.with_temporary_directory('lua-filter', function (dir)+ local filter_path = pandoc.path.join{dir, 'test.lua'}+ local filter = 'function Space() return " " end'+ local fh = io.open(filter_path, 'wb')+ fh:write(filter)+ fh:close()+ assert.are_equal(+ utils.run_lua_filter(doc, filter_path),+ pandoc.Pandoc(+ pandoc.Plain(pandoc.Inlines{"indivisible", " ", "words"})+ )+ )+ end)+ end),+ test("doesn't change the local environment by default", function ()+ pandoc.system.with_temporary_directory('lua-filter', function (dir)+ local filter_path = pandoc.path.join{dir, 'test.lua'}+ local foo+ local filter = 'foo = 42'+ local fh = io.open(filter_path, 'wb')+ fh:write(filter)+ fh:close()+ utils.run_lua_filter(pandoc.Pandoc{}, filter_path)+ assert.is_nil(foo)+ end)+ end),+ test("accepts an environment in which the filter is executed", function ()+ pandoc.system.with_temporary_directory('lua-filter', function (dir)+ local filter_path = pandoc.path.join{dir, 'test.lua'}+ local filter = 'foo = 42'+ local fh = io.open(filter_path, 'wb')+ fh:write(filter)+ fh:close()+ utils.run_lua_filter(pandoc.Pandoc{}, filter_path, _ENV)+ assert.are_equal(_ENV.foo, 42)+ end) end) },