packages feed

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 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)   },