pandoc-lua-engine 0.5.0.2 → 0.5.1
raw patch · 21 files changed
+535/−242 lines, 21 filesdep ~citeprocdep ~hsluadep ~hslua-module-path
Dependency ranges changed: citeproc, hslua, hslua-module-path, hslua-module-system, hslua-module-text, hslua-module-version, hslua-module-zip, pandoc
Files
- pandoc-lua-engine.cabal +18/−17
- src/Text/Pandoc/Lua/Documentation.hs +187/−0
- src/Text/Pandoc/Lua/Init.hs +1/−1
- src/Text/Pandoc/Lua/Marshal/CommonState.hs +2/−0
- src/Text/Pandoc/Lua/Marshal/LogMessage.hs +7/−0
- src/Text/Pandoc/Lua/Module/CLI.hs +17/−17
- src/Text/Pandoc/Lua/Module/Format.hs +3/−8
- src/Text/Pandoc/Lua/Module/Image.hs +3/−16
- src/Text/Pandoc/Lua/Module/JSON.hs +9/−15
- src/Text/Pandoc/Lua/Module/Log.hs +4/−8
- src/Text/Pandoc/Lua/Module/MediaBag.hs +6/−10
- src/Text/Pandoc/Lua/Module/Pandoc.hs +136/−45
- src/Text/Pandoc/Lua/Module/Path.hs +5/−9
- src/Text/Pandoc/Lua/Module/Scaffolding.hs +8/−17
- src/Text/Pandoc/Lua/Module/Structure.hs +6/−11
- src/Text/Pandoc/Lua/Module/System.hs +21/−28
- src/Text/Pandoc/Lua/Module/Template.hs +6/−12
- src/Text/Pandoc/Lua/Module/Text.hs +3/−5
- src/Text/Pandoc/Lua/Module/Types.hs +6/−9
- src/Text/Pandoc/Lua/Module/Utils.hs +45/−14
- test/lua/module/pandoc.lua +42/−0
pandoc-lua-engine.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.4 name: pandoc-lua-engine-version: 0.5.0.2+version: 0.5.1 build-type: Simple license: GPL-2.0-or-later license-file: COPYING.md@@ -10,12 +10,6 @@ bug-reports: https://github.com/jgm/pandoc/issues homepage: https://pandoc.org category: Text-tested-with: GHC == 8.6.5- , GHC == 8.8.4- , GHC == 8.10.7- , GHC == 9.0.2- , GHC == 9.2.5- , GHC == 9.4.4 synopsis: Lua engine to power custom pandoc conversions description: This package provides a pandoc scripting engine based on Lua.@@ -44,6 +38,10 @@ location: https://github.com/jgm/pandoc.git subdir: pandoc-lua-engine +flag repl+ Description: Include pandoc Lua repl.+ Default: True+ common common-options default-language: Haskell2010 build-depends: base >= 4.12 && < 5@@ -69,6 +67,7 @@ hs-source-dirs: src exposed-modules: Text.Pandoc.Lua other-modules: Text.Pandoc.Lua.Custom+ , Text.Pandoc.Lua.Documentation , Text.Pandoc.Lua.Engine , Text.Pandoc.Lua.Filter , Text.Pandoc.Lua.Global@@ -111,28 +110,30 @@ build-depends: aeson , bytestring >= 0.9 && < 0.13 , crypton >= 0.30 && < 1.1- , citeproc >= 0.8 && < 0.13+ , citeproc >= 0.8 && < 0.14 , containers >= 0.6.0.1 && < 0.9 , data-default >= 0.4 && < 0.9 , doclayout >= 0.5 && < 0.6 , doctemplates >= 0.11 && < 0.12 , exceptions >= 0.8 && < 0.11- , hslua >= 2.3 && < 2.5+ , hslua >= 2.5 && < 2.6 , hslua-module-doclayout>= 1.2 && < 1.3- , hslua-module-path >= 1.1 && < 1.2- , hslua-module-system >= 1.2.3 && < 1.3- , hslua-module-text >= 1.1 && < 1.2- , hslua-module-version >= 1.1 && < 1.2- , hslua-module-zip >= 1.1.3 && < 1.2- , hslua-repl >= 0.1.1 && < 0.2+ , hslua-module-path >= 1.2 && < 1.3+ , hslua-module-system >= 1.3 && < 1.4+ , hslua-module-text >= 1.2 && < 1.3+ , hslua-module-version >= 1.2 && < 1.3+ , hslua-module-zip >= 1.1.5 && < 1.3 , lpeg >= 1.1 && < 1.2 , mtl >= 2.2 && < 2.4- , pandoc >= 3.8 && < 3.9+ , pandoc >= 3.9 && < 3.10 , pandoc-lua-marshal >= 0.3 && < 0.4 , pandoc-types >= 1.22 && < 1.24 , parsec >= 3.1 && < 3.2 , text >= 1.1.1 && < 2.2 + if flag(repl)+ build-depends: hslua-repl >= 0.1.1 && < 0.2+ cpp-options: -DINCLUDE_REPL test-suite test-pandoc-lua-engine import: common-options@@ -145,7 +146,7 @@ , data-default , exceptions >= 0.8 && < 0.11 , filepath- , hslua >= 2.3 && < 2.5+ , hslua >= 2.5 && < 2.6 , pandoc , pandoc-types >= 1.22 && < 1.24 , tasty
+ src/Text/Pandoc/Lua/Documentation.hs view
@@ -0,0 +1,187 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{- |+ Module : Text.Pandoc.Lua.Documentation+ Copyright : Copyright © 2026 Albert Krewinkel+ License : GPL-2.0-or-later+ Maintainer : Albert Krewinkel <albert+pandoc@tarleb.com>++Render Lua documentation+-}+module Text.Pandoc.Lua.Documentation+ ( renderDocumentation+ ) where++import Data.Default (def)+import Data.List (intersperse)+import Data.Sequence (Seq ((:|>)))+import Data.Version (showVersion)+import HsLua as Lua+import Text.Pandoc.Class (runPure)+import Text.Pandoc.Definition (Pandoc (Pandoc))+import Text.Pandoc.Extensions (extensionsFromList, Extension (..))+import Text.Pandoc.Options (ReaderOptions (readerExtensions))+import Text.Pandoc.Readers (readCommonMark)+import Text.Pandoc.Shared (compactify)+import Text.Pandoc.Walk (walk)++import qualified Data.Text as T+import qualified Text.Pandoc.Builder as B+import qualified Text.Pandoc.UTF8 as UTF8++-- | Render the documentation object as pandoc Blocks+renderDocumentation :: DocumentationObject -> B.Blocks+renderDocumentation = \case+ DocObjectFunction fn -> renderFunctionDoc Nothing fn+ DocObjectModule mdl -> renderModuleDoc mdl+ DocObjectType tp -> renderTypeDoc Nothing tp++renderTypeDoc :: Maybe T.Text -> TypeDoc -> B.Blocks+renderTypeDoc mbmodname td = mconcat+ [ B.headerWith (ident, [], []) 1 (B.str $ typeDocName td)+ , parseCommonMark $ typeDocDescription td+ , if null $ typeDocMethods td+ then mempty+ else+ B.header 2 "Methods" <>+ (shiftHeadings 2 . mconcat . map (renderFunctionDoc Nothing) $+ typeDocMethods td)+ ]+ where+ ident = case mbmodname of+ Just modname -> mconcat [ "type-", modname, ".", typeDocName td ]+ Nothing -> mconcat [ "type-", typeDocName td ]++-- Shift headings+shiftHeadings :: Int -> B.Blocks -> B.Blocks+shiftHeadings incr blks = flip walk blks $ \case+ B.Header level attr inner -> B.Header (level + incr) attr inner+ x -> x++renderModuleDoc :: ModuleDoc -> B.Blocks+renderModuleDoc moddoc =+ let modname = moduleDocName moddoc+ in mconcat+ [ B.headerWith ("module-" <> modname, [], []) 1+ (B.str $ "Module " <> modname)+ , parseCommonMark (moduleDocDescription moddoc)+ , if null (moduleDocFields moddoc)+ then mempty+ else+ let ident = modname <> "-fields"+ in B.headerWith (ident, [], []) 2 (B.str "Fields") <>+ shiftHeadings 0 (mconcat (map (renderFieldDoc modname)+ (moduleDocFields moddoc)))+ , if null (moduleDocFunctions moddoc)+ then mempty+ else+ let ident = modname <> "-functions"+ in B.headerWith (ident, [], []) 2 (B.str "Functions") <>+ (shiftHeadings 2 . mconcat . map (renderFunctionDoc $ Just modname) $+ moduleDocFunctions moddoc)+ , if null (moduleDocTypes moddoc)+ then mempty+ else+ let ident = modname <> "-types"+ in B.headerWith (ident, [], []) 2 (B.str "Types") <>+ (shiftHeadings 2 . mconcat . map (renderTypeDoc $ Just modname) .+ reverse $ moduleDocTypes moddoc)+ ]++parseCommonMark :: T.Text -> B.Blocks+parseCommonMark txt =+ let exts = extensionsFromList+ [ Ext_wikilinks_title_after_pipe+ , Ext_smart+ ]+ result = runPure $ do+ Pandoc _ blks <- readCommonMark (def {readerExtensions = exts}) txt+ return $ B.fromList blks+ in either mempty id result++appendInlines :: B.Blocks -> B.Inlines -> B.Blocks+appendInlines blks inlns = case B.unMany blks of+ front :|> (B.Para xs) -> B.Many front <> B.para (addTo xs)+ front :|> (B.Plain xs) -> B.Many front <> B.plain (addTo xs)+ _ -> blks <> B.para inlns+ where addTo xs = B.fromList xs <> B.space <> inlns++appendType :: B.Blocks -> TypeSpec -> B.Blocks+appendType blks typespec =+ appendInlines blks (B.str "(" <> typeToInlines typespec <> B.str ")")++typeToInlines :: TypeSpec -> B.Inlines+typeToInlines = \case+ bt@BasicType{} -> builtin $ tystr bt+ NamedType "integer" -> builtin "integer"+ NamedType name -> B.linkWith ("", ["documented-type"], [])+ ("#" <> n2t name) mempty $ B.str (n2t name)+ SeqType itemtype -> "{" <> typeToInlines itemtype <> ",...}"+ SumType summands -> mconcat . intersperse (B.str "|") $ map typeToInlines summands+ AnyType -> "any"+ x -> tystr x+ where+ tystr = B.str . T.pack . typeSpecToString+ n2t = UTF8.toText . fromName+ builtin = B.spanWith ("", ["builtin-lua-type"], [])++renderFunctionDoc :: Maybe T.Text -> FunctionDoc -> B.Blocks+renderFunctionDoc mbmodule fndoc =+ let name = case mbmodule of+ Just _ -> T.takeWhileEnd (/= '.') $ funDocName fndoc+ Nothing -> funDocName fndoc+ ident = funDocName fndoc+ level = 1+ argsString = argslist (funDocParameters fndoc)+ paramToDefItem p = ( B.code $ parameterName p+ , compactify+ [ appendType+ (parseCommonMark $ parameterDescription p)+ (parameterType p)+ ]+ )+ paramlist = B.definitionList . map paramToDefItem $+ funDocParameters fndoc+ in mconcat+ [ B.headerWith (ident, [], []) level (B.str name)+ , B.plain (B.code $ name <> " (" <> argsString <> ")")+ , parseCommonMark (funDocDescription fndoc)+ , if null (funDocParameters fndoc)+ then mempty+ else B.para "Parameters:" <> paramlist+ , if funDocResults fndoc == ResultsDocList []+ then mempty+ else B.para "Returns:" <> renderResults (funDocResults fndoc)+ , case funDocSince fndoc of+ Nothing -> mempty+ Just version ->+ B.para $ B.emph $ "Since: " <> (B.str . T.pack $ showVersion version)+ ]++renderResults :: ResultsDoc -> B.Blocks+renderResults (ResultsDocMult descr) = parseCommonMark descr+renderResults (ResultsDocList rvd) = B.bulletList $ map renderResultVal rvd+ where+ renderResultVal (ResultValueDoc typespec descr) =+ parseCommonMark descr `appendType` typespec++argslist :: [ParameterDoc] -> T.Text+argslist params =+ -- Expect optional values to come after required values.+ let (required, optional') = break parameterIsOptional params+ reqs = map parameterName required+ opts = map parameterName optional'+ in if null opts+ then T.intercalate ", " reqs+ else T.intercalate ", " reqs <>+ (if null required then "[" else "[, ") <>+ T.intercalate "[, " opts <> T.replicate (length opts) "]"++renderFieldDoc :: T.Text -> FieldDoc -> B.Blocks+renderFieldDoc _modname fd =+ B.headerWith (ident, [], []) 3 (B.str name) <>+ appendType (parseCommonMark $ fieldDocDescription fd) (fieldDocType fd)+ where+ ident = fieldDocName fd+ name = T.takeWhileEnd (/= '.') $ fieldDocName fd
src/Text/Pandoc/Lua/Init.hs view
@@ -3,7 +3,7 @@ {-# LANGUAGE RankNTypes #-} {- | Module : Text.Pandoc.Lua.Init- Copyright : © 2017-2024 Albert Krewinkel+ Copyright : © 2017-2026 Albert Krewinkel License : GPL-2.0-or-later Maintainer : Albert Krewinkel <albert+pandoc@tarleb.com>
src/Text/Pandoc/Lua/Marshal/CommonState.hs view
@@ -26,8 +26,10 @@ 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
src/Text/Pandoc/Lua/Marshal/LogMessage.hs view
@@ -13,6 +13,8 @@ , typeLogMessage ) where +import Control.Applicative (optional)+import Data.Maybe (fromMaybe) import HsLua import Text.Pandoc.Logging (LogMessage, showLogMessage) import qualified Data.Aeson as Aeson@@ -24,6 +26,11 @@ ### liftPure showLogMessage <#> udparam typeLogMessage "msg" "object" =#> functionResult pushText "string" "stringified log message"+ , operation Eq $ lambda+ ### liftPure2 (\a b -> fromMaybe False ((==) <$> a <*> b))+ <#> parameter (optional . peekLogMessage) "a" "LogMessage" ""+ <#> parameter (optional . peekLogMessage) "b" "LogMessage" ""+ =#> functionResult pushBool "boolean" "whether the two are equal" , operation (CustomOperation "__tojson") $ lambda ### liftPure Aeson.encode <#> udparam typeLogMessage "msg" "object"
src/Text/Pandoc/Lua/Module/CLI.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {- |@@ -15,28 +16,27 @@ import Control.Applicative ((<|>)) import Data.Version (makeVersion) import HsLua-import HsLua.REPL (defaultConfig, replWithEnv, setup) import Text.Pandoc.App (defaultOpts, options, parseOptionsFromArgs) import Text.Pandoc.Error (PandocError) import Text.Pandoc.Lua.PandocLua () import qualified Data.Text as T+#ifdef INCLUDE_REPL+import HsLua.REPL (defaultConfig, replWithEnv, setup)+#endif -- | Push the pandoc.types module on the Lua stack. documentedModule :: Module PandocError-documentedModule = Module- { moduleName = "pandoc.cli"- , moduleDescription =+documentedModule = defmodule "pandoc.cli"+ `withDescription` "Command line options and argument parsing."- , moduleFields =- [ Field- { fieldName = "default_options"- , fieldType = "table"- , fieldDescription = "Default CLI options, using a JSON-like " <>- "representation."- , fieldPushValue = pushViaJSON defaultOpts- }+ `withFields`+ [ deffield "default_options"+ `withType` "table"+ `withDescription`+ "Default CLI options, using a JSON-like representation."+ `withValue` pushViaJSON defaultOpts ]- , moduleFunctions =+ `withFunctions` [ defun "parse_options" ### parseOptions <#> parameter peekArgs "{string,...}" "args"@@ -49,12 +49,10 @@ , "scripts, taking the list of arguments from the global `arg`." ] `since` makeVersion [3, 0]-+#ifdef INCLUDE_REPL , repl `since` makeVersion [3, 1, 2]+#endif ]- , moduleOperations = []- , moduleTypeInitializers = []- } where peekArgs idx = (,)@@ -67,6 +65,7 @@ Left e -> failLua $ "Cannot process info option: " ++ show e Right opts -> pure opts +#ifdef INCLUDE_REPL -- | Starts a REPL. repl :: DocumentedFunction PandocError repl = defun "repl"@@ -123,3 +122,4 @@ copyval copyval pop 1 -- global table+#endif
src/Text/Pandoc/Lua/Module/Format.hs view
@@ -23,16 +23,11 @@ -- | The "pandoc.format" module. documentedModule :: Module PandocError-documentedModule = Module- { moduleName = "pandoc.format"- , moduleDescription = T.unlines+documentedModule = defmodule "pandoc.format"+ `withDescription` T.unlines [ "Information about the formats supported by pandoc." ]- , moduleFields = []- , moduleOperations = []- , moduleFunctions = functions- , moduleTypeInitializers = []- }+ `withFunctions` functions -- | Extension module functions. functions :: [DocumentedFunction PandocError]
src/Text/Pandoc/Lua/Module/Image.hs view
@@ -34,22 +34,9 @@ -- | The @pandoc.image@ module specification. documentedModule :: Module PandocError-documentedModule = Module- { moduleName = "pandoc.image"- , moduleDescription = "Basic image querying functions."- , moduleFields = fields- , moduleFunctions = functions- , moduleOperations = []- , moduleTypeInitializers = []- }------- Fields------- | Exported fields.-fields :: LuaError e => [Field e]-fields = []+documentedModule = defmodule "pandoc.image"+ `withDescription` "Basic image querying functions."+ `withFunctions` functions -- -- Functions
src/Text/Pandoc/Lua/Module/JSON.hs view
@@ -35,15 +35,11 @@ -- | The @aeson@ module specification. documentedModule :: Module PandocError-documentedModule = Module- { moduleName = "pandoc.json"- , moduleDescription = "JSON module to work with JSON; " <>- "based on the Aeson Haskell package."- , moduleFields = fields- , moduleFunctions = functions- , moduleOperations = []- , moduleTypeInitializers = []- }+documentedModule = defmodule "pandoc.json"+ `withDescription`+ "JSON module to work with JSON; based on the Aeson Haskell package."+ `withFields` fields+ `withFunctions` functions -- -- Fields@@ -57,12 +53,10 @@ -- | The value used to represent the JSON @null@. null :: LuaError e => Field e-null = Field- { fieldName = "null"- , fieldType = "light userdata"- , fieldDescription = "Value used to represent the `null` JSON value."- , fieldPushValue = pushValue Aeson.Null- }+null = deffield "null"+ `withType` "light userdata"+ `withDescription` "Value used to represent the `null` JSON value."+ `withValue` pushValue Aeson.Null -- -- Functions
src/Text/Pandoc/Lua/Module/Log.hs view
@@ -26,12 +26,11 @@ -- | Push the pandoc.log module on the Lua stack. documentedModule :: Module PandocError-documentedModule = Module- { moduleName = "pandoc.log"- , moduleDescription =+documentedModule = defmodule "pandoc.log"+ `withDescription` "Access to pandoc's logging system."- , moduleFields = []- , moduleFunctions =+ `withFields` []+ `withFunctions` [ defun "info" ### (\msg -> do -- reporting levels:@@ -78,9 +77,6 @@ ] `since` makeVersion [3, 2] ]- , moduleOperations = []- , moduleTypeInitializers = []- } -- | Calls the function given as the first argument, but suppresses logging. -- Returns the list of generated log messages as the first result, and the other
src/Text/Pandoc/Lua/Module/MediaBag.hs view
@@ -37,9 +37,8 @@ -- MediaBag submodule -- documentedModule :: Module PandocError-documentedModule = Module- { moduleName = "pandoc.mediabag"- , moduleDescription = T.unlines+documentedModule = Lua.defmodule "pandoc.mediabag"+ `Lua.withDescription` T.unlines [ "The `pandoc.mediabag` module allows accessing pandoc's media" , "storage. The \"media bag\" is used when pandoc is called with the" , "`--extract-media` or (for HTML only) `--embed-resources` option."@@ -50,8 +49,7 @@ , "" , " local mb = require 'pandoc.mediabag'" ]- , moduleFields = []- , moduleFunctions =+ `Lua.withFunctions` [ delete `since` makeVersion [2,7,3] , empty `since` makeVersion [2,7,3] , fetch `since` makeVersion [2,0]@@ -63,9 +61,6 @@ , make_data_uri `since` makeVersion [3,7,1] , write `since` makeVersion [3,0] ]- , moduleOperations = []- , moduleTypeInitializers = []- } -- | Delete a single item from the media bag. delete :: DocumentedFunction PandocError@@ -109,8 +104,9 @@ setMediaBag $ MB.insertMedia fp mmime contents mb return (Lua.NumResults 0)) <#> stringParam "filepath" "filename and path relative to the output folder."- <#> opt (textParam "mimetype"- "the item's MIME type; omit if unknown or unavailable.")+ <#> parameter (Lua.peekNilOr Lua.peekText) "string|nil" "mimetype"+ "the item's MIME type; use `nil` if the MIME type is\+ \ unknown or unavailable." <#> parameter Lua.peekLazyByteString "string" "contents" "the binary contents of the file." =#> []
src/Text/Pandoc/Lua/Module/Pandoc.hs view
@@ -30,6 +30,8 @@ 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@@ -57,22 +59,19 @@ 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 documentedModule :: Module PandocError-documentedModule = Module- { moduleName = "pandoc"- , moduleDescription = T.unlines+documentedModule = defmodule "pandoc"+ `withDescription` T.unlines [ "Fields and functions for pandoc scripts; includes constructors for" , "document tree elements, functions to parse text in a given" , "format, and functions to filter and modify a subtree." ]- , moduleFields = readersField : writersField :+ `withFields` readersField : writersField : stringConstants ++ [inlineField, blockField]- , moduleOperations = []- , moduleFunctions = mconcat+ `withFunctions` mconcat [ [mkPandoc, mkMeta] , metaValueConstructors , blockConstructors@@ -82,60 +81,57 @@ , otherConstructors , functions ]- , moduleTypeInitializers =- [ initType typePandoc- , initType typeBlock- , initType typeInline- ]- }+ `associateType` typePandoc+ `associateType` typeBlock+ `associateType` typeInline -- | Set of input formats accepted by @read@. readersField :: Field PandocError-readersField = Field- { fieldName = "readers"- , fieldType = "table"- , fieldDescription = T.unlines+readersField = deffield "readers"+ `withType` "table"+ `withDescription` 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))- }+ `withValue` pushKeyValuePairs pushText (pushText . readerType)+ (readers @PandocLua)+ where+ readerType = \case+ TextReader {} -> "text"+ ByteStringReader {} -> "bytestring" -- | Set of input formats accepted by @write@. writersField :: Field PandocError-writersField = Field- { fieldName = "writers"- , fieldType = "table"- , fieldDescription = T.unlines+writersField = deffield "writers"+ `withType` "table"+ `withDescription` 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))- }+ `withValue` pushKeyValuePairs pushText (pushText . writerType)+ (writers @PandocLua)+ where+ writerType = \case+ TextWriter {} -> "text"+ ByteStringWriter {} -> "bytestring" -- | Inline table field inlineField :: Field PandocError-inlineField = Field- { fieldName = "Inline"- , fieldType = "table"- , fieldDescription = "Inline constructors, nested under 'constructors'."+inlineField = deffield "Inline"+ `withType` "table"+ `withDescription` "Inline constructors, nested under 'constructors'." -- the nesting happens for historical reasons and should probably be -- changed.- , fieldPushValue = pushWithConstructorsSubtable inlineConstructors- }+ `withValue` pushWithConstructorsSubtable inlineConstructors -- | @Block@ module field blockField :: Field PandocError-blockField = Field- { fieldName = "Block"- , fieldType = "table"- , fieldDescription = "Inline constructors, nested under 'constructors'."+blockField = deffield "Block"+ `withType` "table"+ `withDescription` "Inline constructors, nested under 'constructors'." -- the nesting happens for historical reasons and should probably be -- changed.- , fieldPushValue = pushWithConstructorsSubtable blockConstructors- }+ `withValue` pushWithConstructorsSubtable blockConstructors pushWithConstructorsSubtable :: [DocumentedFunction PandocError] -> LuaE PandocError ()@@ -213,12 +209,10 @@ , constrs (Proxy @Alignment) , constrs (Proxy @CitationMode) ]- toField s = Field- { fieldName = T.pack s- , fieldType = "string"- , fieldDescription = T.pack s- , fieldPushValue = pushString s- }+ toField s = deffield (Name $ UTF8.fromString s)+ `withType` "string"+ `withDescription` T.pack s+ `withValue` pushString s in map toField nullaryConstructors functions :: [DocumentedFunction PandocError]@@ -294,6 +288,37 @@ <#> 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@@ -429,3 +454,69 @@ -- 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+ absidx <- liftLua $ absindex idx+ let setOptions opts = do+ liftLua (next absidx) >>= \case+ False -> return opts+ True -> do+ key <- peekByteString (nth 2)+ case key of+ "request_headers" -> do+ let peekHeaderPair = peekPair peekText peekText+ value <- peekList peekHeaderPair top `lastly` pop 1+ setOptions $ opts { stateOptsRequestHeaders = value }+ "resource_path" -> do+ value <- peekList peekString top `lastly` pop 1+ setOptions $ opts { stateOptsResourcePath = value }+ "user_data_dir" -> do+ value <- peekNilOr peekString top `lastly` pop 1+ setOptions $ opts { stateOptsUserDataDir = value }+ _ -> do+ liftLua $ pop 2 -- remove key and value+ failPeek $ "Unknown or unsupported state option: " <> key++ liftLua pushnil -- first "key"+ liftLua getStateOptions >>= setOptions++-- | 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
src/Text/Pandoc/Lua/Module/Path.hs view
@@ -3,7 +3,7 @@ {-# LANGUAGE TypeApplications #-} {- | Module : Text.Pandoc.Lua.Module.Path- Copyright : © 2019-2024 Albert Krewinkel+ Copyright : © 2019-2026 Albert Krewinkel License : GNU GPL, version 2 or above Maintainer : Albert Krewinkel <albert+pandoc@tarleb.com>@@ -22,14 +22,13 @@ -- | Push the pandoc.system module on the Lua stack. documentedModule :: forall e. LuaError e => Module e-documentedModule = Module- { moduleName = "pandoc.path"- , moduleDescription = moduleDescription @e MPath.documentedModule- , moduleFields =+documentedModule = defmodule "pandoc.path"+ `withDescription` moduleDescription @e MPath.documentedModule+ `withFields` [ MPath.separator , MPath.search_path_separator ]- , moduleFunctions =+ `withFunctions` [ MPath.directory `since` v[2,12] , MSystem.exists `since` v[3,7,1] , MPath.filename `since` v[2,12]@@ -43,8 +42,5 @@ , MPath.split_search_path `since` v[2,12] , MPath.treat_strings_as_paths `since` v[2,12] ]- , moduleOperations = []- , moduleTypeInitializers = []- } where v = makeVersion
src/Text/Pandoc/Lua/Module/Scaffolding.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE OverloadedStrings #-} {- | Module : Text.Pandoc.Lua.Module.Scaffolding- Copyright : Copyright © 2022-2024 Albert Krewinkel, John MacFarlane+ Copyright : Copyright © 2022-2026 Albert Krewinkel, John MacFarlane License : GNU GPL, version 2 or above Maintainer : Albert Krewinkel <albert+pandoc@tarleb.com> @@ -18,28 +18,20 @@ -- | The "pandoc.template" module. documentedModule :: Module PandocError-documentedModule = Module- { moduleName = "pandoc.scaffolding"- , moduleDescription = T.unlines- [ "Scaffolding for custom writers."- ]- , moduleFields = [writerScaffolding]- , moduleOperations = []- , moduleFunctions = []- , moduleTypeInitializers = []- }+documentedModule = defmodule "pandoc.scaffolding"+ `withDescription` "Scaffolding for custom writers."+ `withFields` [writerScaffolding] -- | Template module functions. writerScaffolding :: Field PandocError-writerScaffolding = Field- { fieldName = "Writer"- , fieldType = "table"- , fieldDescription = T.unlines+writerScaffolding = deffield "Writer"+ `withType` "table"+ `withDescription` T.unlines [ "An object to be used as a `Writer` function; the construct handles" , "most of the boilerplate, expecting only render functions for all" , "AST elements" ]- , fieldPushValue = do+ `withValue` do pushWriterScaffolding -- pretend that it's a submodule so we get better error messages getfield registryindex loaded@@ -50,5 +42,4 @@ getfield (nth 2) "Block" *> setfield (nth 2) (submod "Writer.Block") pop 1 -- remove "LOADED_TABLE"- } where submod x = moduleName documentedModule <> "." <> x
src/Text/Pandoc/Lua/Module/Structure.hs view
@@ -2,7 +2,7 @@ {-# LANGUAGE OverloadedStrings #-} {- | Module : Text.Pandoc.Lua.Module.Structure- Copyright : © 2023-2025 Albert Krewinkel <albert+pandoc@tarleb.com>+ Copyright : © 2023-2026 Albert Krewinkel <albert+pandoc@tarleb.com> License : GPL-2.0-or-later Maintainer : Albert Krewinkel <albert+pandoc@tarleb.com> @@ -17,11 +17,11 @@ import Data.Maybe (fromMaybe) import Data.Version (makeVersion) import HsLua ( DocumentedFunction, LuaError, Module (..), Peeker- , (###), (<#>), (=#>), (#?)+ , (###), (<#>), (=#>), (#?), defmodule , defun, functionResult, getfield, isnil, lastly, liftLua , opt, liftPure, parameter , peekBool, peekIntegral , peekFieldRaw, peekSet, peekText, pop, pushIntegral- , pushText, since, top )+ , pushText, since, top, withDescription, withFunctions ) import Text.Pandoc.Chunks ( ChunkedDoc (..), PathTemplate (..) , tocToList, splitIntoChunks ) import Text.Pandoc.Definition (Pandoc (..), Block)@@ -41,22 +41,17 @@ -- | Push the pandoc.structure module on the Lua stack. documentedModule :: Module PandocError-documentedModule = Module- { moduleName = "pandoc.structure"- , moduleDescription =+documentedModule = defmodule "pandoc.structure"+ `withDescription` "Access to the higher-level document structure, including " <> "hierarchical sections and the table of contents."- , moduleFields = []- , moduleFunctions =+ `withFunctions` [ make_sections `since` makeVersion [3,0] , slide_level `since` makeVersion [3,0] , split_into_chunks `since` makeVersion [3,0] , table_of_contents `since` makeVersion [3,0] , unique_identifier `since` makeVersion [3,8] ]- , moduleOperations = []- , moduleTypeInitializers = []- } make_sections :: LuaError e => DocumentedFunction e make_sections = defun "make_sections"
src/Text/Pandoc/Lua/Module/System.hs view
@@ -26,34 +26,27 @@ -- | Push the pandoc.system module on the Lua stack. documentedModule :: forall e. LuaError e => Module e-documentedModule = Module- { moduleName = "pandoc.system"- , moduleDescription = moduleDescription @e MSys.documentedModule- , moduleFields =- [ arch- , os- ]- , moduleFunctions =- [ cputime `since` v[3,1,1]- , setName "command" cmd `since` v[3,7,1]- , setName "copy" cp `since` v[3,7,1]- , setName "environment" env `since` v[2,7,3]- , setName "get_working_directory" getwd `since` v[2,8]- , setName "list_directory" ls `since` v[2,19]- , setName "make_directory" mkdir `since` v[2,19]- , read_file `since` v[3,7,1]- , rename `since` v[3,7,1]- , setName "remove" rm `since` v[3,7,1]- , setName "remove_directory" rmdir `since` v[2,19]- , times `since` v[3,7,1]- , setName "with_environment" with_env `since` v[2,7,3]- , setName "with_temporary_directory" with_tmpdir `since` v[2,8]- , setName "with_working_directory" with_wd `since` v[2,7,3]- , write_file `since` v[3,7,1]- , xdg `since` v[3,7,1]+documentedModule = defmodule "pandoc.system"+ `withDescription` moduleDescription @e MSys.documentedModule+ `withFields` [arch, os]+ `withFunctions`+ [ cputime `since` v[3,1,1]+ , setName cmd "command" `since` v[3,7,1]+ , setName cp "copy" `since` v[3,7,1]+ , setName env "environment" `since` v[2,7,3]+ , setName getwd "get_working_directory" `since` v[2,8]+ , setName ls "list_directory" `since` v[2,19]+ , setName mkdir "make_directory" `since` v[2,19]+ , read_file `since` v[3,7,1]+ , rename `since` v[3,7,1]+ , setName rm "remove" `since` v[3,7,1]+ , setName rmdir "remove_directory" `since` v[2,19]+ , times `since` v[3,7,1]+ , setName with_env "with_environment" `since` v[2,7,3]+ , setName with_tmpdir "with_temporary_directory" `since` v[2,8]+ , setName with_wd "with_working_directory" `since` v[2,7,3]+ , write_file `since` v[3,7,1]+ , xdg `since` v[3,7,1] ]- , moduleOperations = []- , moduleTypeInitializers = []- } where v = makeVersion
src/Text/Pandoc/Lua/Module/Template.hs view
@@ -1,8 +1,8 @@ {-# LANGUAGE OverloadedStrings #-} {- | Module : Text.Pandoc.Lua.Module.Template- Copyright : Copyright © 2022-2024 Albert Krewinkel, John MacFarlane- License : GNU GPL, version 2 or above+ Copyright : Copyright © 2022-2026 Albert Krewinkel, John MacFarlane+ License : GPL-2.0-or-later Maintainer : Albert Krewinkel <albert+pandoc@tarleb.com> Lua module to handle pandoc templates.@@ -28,16 +28,10 @@ -- | The "pandoc.template" module. documentedModule :: Module PandocError-documentedModule = Module- { moduleName = "pandoc.template"- , moduleDescription = T.unlines- [ "Handle pandoc templates."- ]- , moduleFields = []- , moduleOperations = []- , moduleFunctions = functions- , moduleTypeInitializers = [initType typeTemplate]- }+documentedModule = defmodule "pandoc.template"+ `withDescription` "Handle pandoc templates."+ `withFunctions` functions+ `associateType` typeTemplate -- | Template module functions. functions :: [DocumentedFunction PandocError]
src/Text/Pandoc/Lua/Module/Text.hs view
@@ -22,9 +22,8 @@ -- | The @aeson@ module specification. documentedModule :: Module PandocError-documentedModule = TM.documentedModule- { moduleName = "pandoc.text"- , moduleFunctions =+documentedModule = defmodule "pandoc.text"+ `withFunctions` [ TM.fromencoding `since` v[3,0] , TM.len `since` v[2,0,3] , TM.lower `since` v[2,0,3]@@ -35,7 +34,7 @@ , TM.toencoding `since` v[3,0] , TM.upper `since` v[2,0,3] ]- , moduleDescription = T.unlines+ `withDescription` T.unlines [ "UTF-8 aware text manipulation functions, implemented in Haskell." , "" , "The text module can also be loaded under the name `text`, although"@@ -49,7 +48,6 @@ , "end" , "```" ]- } where v = makeVersion
src/Text/Pandoc/Lua/Module/Types.hs view
@@ -15,19 +15,19 @@ import Data.Version (makeVersion) import HsLua ( Module (..), (###), (<#>), (=#>)- , defun, functionResult, parameter, since)+ , defmodule, defun, functionResult, parameter, since+ , withDescription, withFunctions+ ) import HsLua.Module.Version (peekVersionFuzzy, pushVersion) import Text.Pandoc.Error (PandocError) import Text.Pandoc.Lua.PandocLua () -- | Push the pandoc.types module on the Lua stack. documentedModule :: Module PandocError-documentedModule = Module- { moduleName = "pandoc.types"- , moduleDescription =+documentedModule = defmodule "pandoc.types"+ `withDescription` "Constructors for types that are not part of the pandoc AST."- , moduleFields = []- , moduleFunctions =+ `withFunctions` [ defun "Version" ### return <#> parameter peekVersionFuzzy "string|number|{integer,...}|Version"@@ -40,6 +40,3 @@ =#> functionResult pushVersion "Version" "New Version object." `since` makeVersion [2,7,3] ]- , moduleOperations = []- , moduleTypeInitializers = []- }
src/Text/Pandoc/Lua/Module/Utils.hs view
@@ -4,7 +4,7 @@ {-# LANGUAGE TypeApplications #-} {- | Module : Text.Pandoc.Lua.Module.Utils- Copyright : Copyright © 2017-2024 Albert Krewinkel+ Copyright : © 2017-2026 Albert Krewinkel License : GNU GPL, version 2 or above Maintainer : Albert Krewinkel <albert+pandoc@tarleb.com>@@ -19,6 +19,7 @@ import Control.Applicative ((<|>)) import Control.Monad ((<$!>))+import Control.Monad.Except (MonadError (throwError)) import Crypto.Hash (hashWith, SHA1(SHA1)) import Data.Data (showConstr, toConstr) import Data.Default (def)@@ -28,12 +29,17 @@ import HsLua.Module.Version (peekVersionFuzzy, pushVersion) import Text.Pandoc.Citeproc (getReferences, processCitations) import Text.Pandoc.Definition-import Text.Pandoc.Error (PandocError)+import Text.Pandoc.Error (PandocError (PandocLuaError)) import Text.Pandoc.Filter (applyJSONFilter)+import Text.Pandoc.Format (FlavoredFormat (formatName), parseFlavoredFormat)+import Text.Pandoc.Lua.Documentation (renderDocumentation) import Text.Pandoc.Lua.Filter (runFilterFile') import Text.Pandoc.Lua.Marshal.AST+import Text.Pandoc.Lua.Marshal.Format (peekFlavoredFormat) import Text.Pandoc.Lua.Marshal.Reference import Text.Pandoc.Lua.PandocLua (PandocLua (unPandocLua))+import Text.Pandoc.Options (WriterOptions (writerExtensions))+import Text.Pandoc.Writers (Writer (..), getWriter) import qualified Data.Map as Map import qualified Data.Text as T@@ -44,18 +50,15 @@ -- | Push the "pandoc.utils" module to the Lua stack. documentedModule :: Module PandocError-documentedModule = Module- { moduleName = "pandoc.utils"- , moduleDescription = T.unlines+documentedModule = defmodule "pandoc.utils"+ `withDescription` T.unlines [ "This module exposes internal pandoc functions and utility" , "functions." ]- , moduleFields = []- , moduleOperations = []- , moduleTypeInitializers = []- , moduleFunctions =+ `withFunctions` [ blocks_to_inlines `since` v[2,2,3] , citeproc `since` v[2,19,1]+ , documentation `since` v[3,8,4] , equals `since` v[2,5] , from_simple_table `since` v[2,11] , make_sections `since` v[2,8]@@ -71,13 +74,11 @@ , defun "Version" ### liftPure (id @Version)- <#> parameter peekVersionFuzzy- "version string, list of integers, or integer"+ <#> parameter peekVersionFuzzy "Version|string|{integer,...}|number" "v" "version description" =#> functionResult pushVersion "Version" "new Version object" #? "Creates a Version object." ]- } where v = makeVersion @@ -130,6 +131,35 @@ , " end" ] +documentation :: DocumentedFunction PandocError+documentation = defun "documentation"+ ### (\idx mformat -> do+ docobj <- getdocumentation idx >>= \case+ TypeNil -> fail "Undocumented object"+ _ -> forcePeek $ peekDocumentationObject top+ let blocks = renderDocumentation docobj+ if maybe mempty formatName mformat == "blocks"+ then pure . Left $ B.toList blocks+ else unPandocLua $ do+ flvrd <- maybe (parseFlavoredFormat "ansi") pure mformat+ getWriter flvrd >>= \case+ (TextWriter w, es) -> Right <$>+ w def{ writerExtensions = es } (B.doc blocks)+ _ -> throwError $ PandocLuaError+ "ByteString writers are not supported here.")+ <#> parameter pure "any" "object" "Retrieve documentation for this object"+ <#> opt (parameter peekFlavoredFormat "string|table" "format"+ "result format; defaults to `'ansi'`")+ =#> functionResult (either pushBlocks pushText) "string|Blocks"+ "rendered documentation"+ #? "Return the documentation for a function or module defined by pandoc.\+ \ Throws an error if there is no documentation for the given object.\n\+ \\n\+ \The result format can be any textual format accepted by `pandoc.write`,\+ \ and the documentation will be returned in that format.\+ \ Additionally, the special format `blocks` is accepted, in which case\+ \ the documentation is returned as [[Blocks]]."+ equals :: LuaError e => DocumentedFunction e equals = defun "equals" ### equal@@ -210,7 +240,7 @@ normalize_date = defun "normalize_date" ### liftPure Shared.normalizeDate <#> parameter peekText "string" "date" "the date string"- =#> functionResult (maybe pushnil pushText) "string or nil"+ =#> functionResult (maybe pushnil pushText) "string|nil" "normalized date, or nil if normalization failed." #? T.unwords [ "Parse a date and convert (if possible) to \"YYYY-MM-DD\" format. We"@@ -325,7 +355,8 @@ , (fmap (const "") . peekAttr) , (fmap (const "") . peekListAttributes) ] idx)- <#> parameter pure "AST element" "element" "some pandoc AST element"+ <#> parameter pure "Pandoc|Block|Inline|Caption|Cell|MetaValue"+ "element" "some pandoc AST element" =#> functionResult pushText "string" "A plain string representation of the given element." #? T.unlines
test/lua/module/pandoc.lua view
@@ -463,6 +463,48 @@ 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 trigger an error', function ()+ local orig_log = PANDOC_STATE.log+ local opts = {log = 'nonsense'}+ assert.error_matches(+ function ()+ pandoc.with_state(opts, function ()+ assert.are_same(PANDOC_STATE.log, orig_log)+ end)+ end,+ "Unknown or unsupported"+ )+ assert.are_same(PANDOC_STATE.log, orig_log)+ end),+ },+ group 'Marshal' { group 'Inlines' { test('Strings are broken into words', function ()