aboutsummaryrefslogtreecommitdiff
path: root/pandoc-lua-engine
diff options
context:
space:
mode:
authorAlbert Krewinkel <[email protected]>2026-01-02 13:30:47 +0100
committerJohn MacFarlane <[email protected]>2026-01-02 11:10:09 -0500
commit8f8ea44d02b6a3ca0c3e80eaf78e926889f29c9b (patch)
treed5c93a91e98838bcb916f24b8ca8b97fbc09d699 /pandoc-lua-engine
parente8d140f41feb0ad023c0b7f22355fec96207dfee (diff)
Lua: add function `pandoc.with_state`
The function allows to run a callback with a modified pandoc state. This provides the ability to temporarily modify the resource path, the user data directory, and the HTTP request headers. Closes: #10859
Diffstat (limited to 'pandoc-lua-engine')
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/CommonState.hs2
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Pandoc.hs91
-rw-r--r--pandoc-lua-engine/test/lua/module/pandoc.lua37
3 files changed, 130 insertions, 0 deletions
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/CommonState.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/CommonState.hs
index 3f705616d..448976868 100644
--- a/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/CommonState.hs
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Marshal/CommonState.hs
@@ -26,8 +26,10 @@ import Text.Pandoc.Class (CommonState)
typeCommonState :: LuaError e => DocumentedType e CommonState
typeCommonState = deftype "CommonState" [] []
+-- | Retrieves the common state from Lua
peekCommonState :: LuaError e => Peeker e CommonState
peekCommonState = peekUD typeCommonState
+-- | Pushes the common pandoc state to the Lua stack.
pushCommonState :: LuaError e => Pusher e CommonState
pushCommonState = pushUD typeCommonState
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Pandoc.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Pandoc.hs
index 7c0408d63..56cb52910 100644
--- a/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Pandoc.hs
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Pandoc.hs
@@ -30,6 +30,8 @@ import HsLua
import System.Exit (ExitCode (..))
import Text.Pandoc.Class ( PandocMonad, FileInfo (..), FileTree
, addToFileTree, getCurrentTime
+ , getRequestHeaders, getResourcePath, getUserDataDir
+ , setRequestHeaders, setResourcePath, setUserDataDir
, insertInFileTree, sandboxWithFileTree
)
import Text.Pandoc.Definition
@@ -301,6 +303,37 @@ functions =
<#> parameter peekFilter "Filter" "lua_filter" "filter functions"
=#> functionResult pushInline "Inline" "modified Inline"
+ , defun "with_state"
+ ### with_state
+ <#> parameter peekStateOptions "table" "options" "state options"
+ <#> parameter pure "function" "callback"
+ "The action to run with the given state."
+ =?> "The results of the call to *callback*."
+ #? "Runs a function with a modified pandoc state.\n\
+ \\n\
+ \The given callback is invoked after setting the pandoc state to the\
+ \ given values. The modifiable options are restored to their original\
+ \ values once the callback has returned.\n\
+ \\n\
+ \The following state variables can be controlled:\n\
+ \\n\
+ \ - `request_headers` (list of key-value tuples)\n\
+ \ - `resource_path` (list of filepaths)\n\
+ \ - `user_data_dir` (string)\n\
+ \\n\
+ \Other options are ignored, and the rest of the state is not modified.\n\
+ \\n\
+ \Usage:\n\
+ \\n\
+ \ local opts = {\n\
+ \ request_headers = {\n\
+ \ {'Authorization', 'Basic my-secret'}\n\
+ \ }\n\
+ \ }\n\
+ \ pandoc.with_state(opts, function ()\n\
+ \ local mime, contents = pandoc.mediabag.fetch(image_url)\n\
+ \ )\n"
+
, defun "write"
### (\doc mformatspec mwriterOpts -> unPandocLua $ do
flvrd <- maybe (parseFlavoredFormat "markdown") pure mformatspec
@@ -436,3 +469,61 @@ peekReadEnv idx = do
-- Return ersatz file system.
pure tree2
+
+-- | Helper type that holds all common state values that can be controlled.
+--
+-- This is closely related to "CommonState", but that's an opaque value
+-- that can only be read and modified through accessor functions. All
+-- fields in this type can be modified through accessors.
+data StateOptions = StateOptions
+ { stateOptsRequestHeaders :: [(T.Text, T.Text)]
+ , stateOptsResourcePath :: [String]
+ , stateOptsUserDataDir :: Maybe String
+ }
+
+-- | Peek pandoc state options; the current state properties are used for
+-- unspecified values.
+peekStateOptions :: Peeker PandocError StateOptions
+peekStateOptions idx = do
+ opts <- liftLua getStateOptions
+ let peekStateField field defVal peeker =
+ peekFieldRaw (fmap (fromMaybe defVal) . peekNilOr peeker) field idx
+ let peekOptStateField field defVal peeker =
+ peekFieldRaw (fmap (maybe defVal Just ) . peekNilOr peeker) field idx
+ StateOptions
+ <$> peekStateField "request_headers"
+ (stateOptsRequestHeaders opts)
+ (peekList (peekPair peekText peekText))
+ <*> peekStateField "resource_path"
+ (stateOptsResourcePath opts)
+ (peekList peekString)
+ <*> peekOptStateField "user_data_dir"
+ (stateOptsUserDataDir opts)
+ peekString
+
+-- | Get the current options values from the pandoc state.
+getStateOptions :: LuaE PandocError StateOptions
+getStateOptions = unPandocLua $ StateOptions
+ <$> getRequestHeaders
+ <*> getResourcePath
+ <*> getUserDataDir
+
+-- | Update the pandoc state with the new options.
+setStateOptions :: StateOptions -> LuaE PandocError ()
+setStateOptions opts = unPandocLua $ do
+ setRequestHeaders $ stateOptsRequestHeaders opts
+ setResourcePath $ stateOptsResourcePath opts
+ setUserDataDir $ stateOptsUserDataDir opts
+
+-- | Run a callback with a modified pandoc state.
+with_state :: StateOptions -> StackIndex -> LuaE PandocError NumResults
+with_state options callback_idx = do
+ origState <- getStateOptions
+ setStateOptions options
+ -- Invoke the callback
+ oldTop <- gettop
+ pushvalue callback_idx
+ call 0 multret
+ newTop <- gettop
+ setStateOptions origState
+ return . NumResults . fromStackIndex $ newTop - oldTop
diff --git a/pandoc-lua-engine/test/lua/module/pandoc.lua b/pandoc-lua-engine/test/lua/module/pandoc.lua
index 3d745b428..1bb364f54 100644
--- a/pandoc-lua-engine/test/lua/module/pandoc.lua
+++ b/pandoc-lua-engine/test/lua/module/pandoc.lua
@@ -463,6 +463,43 @@ return {
end),
},
+ group 'with_state' {
+ test('request_headers can be modified', function ()
+ local headers = {
+ {"Authorization", "Basic my-secret"}
+ }
+ pandoc.with_state({request_headers = headers}, function ()
+ assert.are_same(PANDOC_STATE.request_headers, headers)
+ end)
+ end),
+ test('resource_path can be modified', function ()
+ local paths = {'.', '/test/resource/path' }
+ pandoc.with_state({resource_path = paths}, function ()
+ assert.are_same(PANDOC_STATE.resource_path, paths)
+ end)
+ end),
+ test('user_data_dir can be modified', function ()
+ local opts = {user_data_dir = '/my/test/path'}
+ pandoc.with_state(opts, function ()
+ assert.are_equal(PANDOC_STATE.user_data_dir, '/my/test/path')
+ end)
+ end),
+ test('original value is restored afterwards', function ()
+ local orig_user_data_dir = PANDOC_STATE.user_data_dir
+ local opts = {user_data_dir = '/my/test/path'}
+ pandoc.with_state(opts, function () end)
+ assert.are_equal(PANDOC_STATE.user_data_dir, orig_user_data_dir)
+ end),
+ test('unsupported options are ignored', function ()
+ local orig_log = PANDOC_STATE.log
+ local opts = {log = 'nonsense'}
+ pandoc.with_state(opts, function ()
+ assert.are_same(PANDOC_STATE.log, orig_log)
+ end)
+ assert.are_same(PANDOC_STATE.log, orig_log)
+ end),
+ },
+
group 'Marshal' {
group 'Inlines' {
test('Strings are broken into words', function ()