diff options
| author | Albert Krewinkel <[email protected]> | 2022-09-29 17:24:31 +0200 |
|---|---|---|
| committer | John MacFarlane <[email protected]> | 2022-09-30 08:33:40 -0700 |
| commit | 5be9052f5fb7283372b3d5497bef499718a34992 (patch) | |
| tree | 80e5805786ef7ab08f363135861e1aa9c8868f6f /pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Pandoc.hs | |
| parent | 79980eee4a1854921d7fb8b14848894b53cc21a7 (diff) | |
[API Change] Extract Lua code into new package pandoc-lua-engine
The flag 'lua53` must now be used with that package if pandoc is to be
compiled against Lua 5.3.
Diffstat (limited to 'pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Pandoc.hs')
| -rw-r--r-- | pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Pandoc.hs | 320 |
1 files changed, 320 insertions, 0 deletions
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Pandoc.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Pandoc.hs new file mode 100644 index 000000000..e708f4345 --- /dev/null +++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Pandoc.hs @@ -0,0 +1,320 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} +{- | + Module : Text.Pandoc.Lua.Module.Pandoc + Copyright : Copyright © 2017-2022 Albert Krewinkel + License : GNU GPL, version 2 or above + + Maintainer : Albert Krewinkel <[email protected]> + Stability : alpha + +Pandoc module for lua. +-} +module Text.Pandoc.Lua.Module.Pandoc + ( pushModule + , documentedModule + ) where + +import Prelude hiding (read) +import Control.Applicative ((<|>)) +import Control.Monad (forM_, when) +import Control.Monad.Catch (catch, throwM) +import Data.Data (Data, dataTypeConstrs, dataTypeOf, showConstr) +import Data.Default (Default (..)) +import Data.Maybe (fromMaybe) +import Data.Proxy (Proxy (Proxy)) +import HsLua hiding (pushModule) +import System.Exit (ExitCode (..)) +import Text.Pandoc.Definition +import Text.Pandoc.Error (PandocError (..)) +import Text.Pandoc.Lua.Orphans () +import Text.Pandoc.Lua.Marshal.AST +import Text.Pandoc.Lua.Marshal.Filter (peekFilter) +import Text.Pandoc.Lua.Marshal.ReaderOptions ( peekReaderOptions + , pushReaderOptions) +import Text.Pandoc.Lua.Marshal.Sources (peekSources) +import Text.Pandoc.Lua.Marshal.WriterOptions ( peekWriterOptions + , pushWriterOptions) +import Text.Pandoc.Lua.Module.Utils (sha1) +import Text.Pandoc.Lua.PandocLua (PandocLua (unPandocLua), liftPandocLua) +import Text.Pandoc.Options ( ReaderOptions (readerExtensions) + , WriterOptions (writerExtensions) ) +import Text.Pandoc.Process (pipeProcess) +import Text.Pandoc.Readers (Reader (..), getReader, readers) +import Text.Pandoc.Sources (toSources) +import Text.Pandoc.Writers (Writer (..), getWriter, writers) + +import qualified HsLua as Lua +import qualified Data.ByteString.Lazy as BL +import qualified Data.ByteString.Lazy.Char8 as BSL +import qualified Data.Set as Set +import qualified Data.Text as T +import qualified Text.Pandoc.UTF8 as UTF8 + +-- | Push the "pandoc" package to the Lua stack. Requires the `List` +-- module to be loadable. +pushModule :: PandocLua NumResults +pushModule = do + liftPandocLua $ Lua.pushModule documentedModule + return 1 + +documentedModule :: Module PandocError +documentedModule = Module + { moduleName = "pandoc" + , moduleDescription = T.unlines + [ "Lua functions for pandoc scripts; includes constructors for" + , "document elements, functions to parse text in a given" + , "format, and functions to filter and modify a subtree." + ] + , moduleFields = readersField : writersField : + stringConstants ++ [inlineField, blockField] + , moduleOperations = [] + , moduleFunctions = mconcat + [ functions + , otherConstructors + , blockConstructors + , inlineConstructors + , metaValueConstructors + ] + } + +-- | Set of input formats accepted by @read@. +readersField :: Field PandocError +readersField = Field + { fieldName = "readers" + , fieldDescription = T.unlines + [ "Set of formats that pandoc can parse. All keys in this table can" + , "be used as the `format` value in `pandoc.read`." + ] + , fieldPushValue = pushSet pushText $ + Set.fromList (map fst (readers @PandocLua)) + } + +-- | Set of input formats accepted by @write@. +writersField :: Field PandocError +writersField = Field + { fieldName = "writers" + , fieldDescription = T.unlines + [ "Set of formats that pandoc can generate. All keys in this table" + , "can be used as the `format` value in `pandoc.write`." + ] + , fieldPushValue = pushSet pushText $ + Set.fromList (map fst (writers @PandocLua)) + } + +-- | Inline table field +inlineField :: Field PandocError +inlineField = Field + { fieldName = "Inline" + , fieldDescription = "Inline constructors, nested under 'constructors'." + -- the nesting happens for historical reasons and should probably be + -- changed. + , fieldPushValue = pushWithConstructorsSubtable inlineConstructors + } + +-- | @Block@ module field +blockField :: Field PandocError +blockField = Field + { fieldName = "Block" + , fieldDescription = "Inline constructors, nested under 'constructors'." + -- the nesting happens for historical reasons and should probably be + -- changed. + , fieldPushValue = pushWithConstructorsSubtable blockConstructors + } + +pushWithConstructorsSubtable :: [DocumentedFunction PandocError] + -> LuaE PandocError () +pushWithConstructorsSubtable constructors = do + newtable -- Field table + newtable -- constructor table + pushName "constructor" *> pushvalue (nth 2) *> rawset (nth 4) + forM_ constructors $ \fn -> do + pushName (functionName fn) + pushDocumentedFunction fn + rawset (nth 3) + pop 1 -- pop constructor table + +otherConstructors :: LuaError e => [DocumentedFunction e] +otherConstructors = + [ mkPandoc + , mkMeta + , mkAttr + , mkAttributeList + , mkBlocks + , mkCitation + , mkCell + , mkRow + , mkTableHead + , mkTableFoot + , mkInlines + , mkListAttributes + , mkSimpleTable + + , defun "ReaderOptions" + ### liftPure id + <#> parameter peekReaderOptions "ReaderOptions|table" "opts" "reader options" + =#> functionResult pushReaderOptions "ReaderOptions" "new object" + #? "Creates a new ReaderOptions value." + + , defun "WriterOptions" + ### liftPure id + <#> parameter peekWriterOptions "WriterOptions|table" "opts" + "writer options" + =#> functionResult pushWriterOptions "WriterOptions" "new object" + #? "Creates a new WriterOptions value." + ] + +stringConstants :: [Field e] +stringConstants = + let constrs :: forall a. Data a => Proxy a -> [String] + constrs _ = map showConstr . dataTypeConstrs . dataTypeOf @a $ undefined + nullaryConstructors = mconcat + [ constrs (Proxy @ListNumberStyle) + , constrs (Proxy @ListNumberDelim) + , constrs (Proxy @QuoteType) + , constrs (Proxy @MathType) + , constrs (Proxy @Alignment) + , constrs (Proxy @CitationMode) + ] + toField s = Field + { fieldName = T.pack s + , fieldDescription = T.pack s + , fieldPushValue = pushString s + } + in map toField nullaryConstructors + +functions :: [DocumentedFunction PandocError] +functions = + [ defun "pipe" + ### (\command args input -> do + (ec, output) <- Lua.liftIO $ pipeProcess Nothing command args input + `catch` (throwM . PandocIOError "pipe") + case ec of + ExitSuccess -> 1 <$ Lua.pushLazyByteString output + ExitFailure n -> do + pushPipeError (PipeError (T.pack command) n output) + Lua.error) + <#> parameter peekString "string" "command" "path to executable" + <#> parameter (peekList peekString) "{string,...}" "args" + "list of arguments" + <#> parameter peekLazyByteString "string" "input" + "input passed to process via stdin" + =?> "output string, or error triple" + + , defun "read" + ### (\content mformatspec mreaderOptions -> do + let formatSpec = fromMaybe "markdown" mformatspec + readerOpts = fromMaybe def mreaderOptions + readAction = getReader formatSpec >>= \case + (TextReader r, es) -> + r readerOpts{readerExtensions = es} + (case content of + Left bs -> toSources $ UTF8.toText bs + Right sources -> sources) + (ByteStringReader r, es) -> + case content of + Left bs -> r readerOpts{readerExtensions = es} + (BSL.fromStrict bs) + Right _ -> liftPandocLua $ Lua.failLua + "Cannot use bytestring reader with Sources" + try (unPandocLua readAction) >>= \case + Right pd -> + -- success, got a Pandoc document + return pd + Left (PandocUnknownReaderError f) -> + Lua.failLua . T.unpack $ "Unknown reader: " <> f + Left (PandocUnsupportedExtensionError e f) -> + Lua.failLua . T.unpack $ + "Extension " <> e <> " not supported for " <> f + Left e -> + throwM e) + <#> parameter (\idx -> (Left <$> peekByteString idx) + <|> (Right <$> peekSources idx)) + "string|Sources" "content" "text to parse" + <#> opt (textParam "formatspec" "format and extensions") + <#> opt (parameter peekReaderOptions "ReaderOptions" "reader_options" + "reader options") + =#> functionResult pushPandoc "Pandoc" "result document" + + , sha1 + + , defun "walk_block" + ### walkElement + <#> parameter peekBlockFuzzy "Block" "block" "element to traverse" + <#> parameter peekFilter "Filter" "lua_filter" "filter functions" + =#> functionResult pushBlock "Block" "modified Block" + + , defun "walk_inline" + ### walkElement + <#> parameter peekInlineFuzzy "Inline" "inline" "element to traverse" + <#> parameter peekFilter "Filter" "lua_filter" "filter functions" + =#> functionResult pushInline "Inline" "modified Inline" + + , defun "write" + ### (\doc mformatspec mwriterOpts -> do + let formatSpec = fromMaybe "html" mformatspec + writerOpts = fromMaybe def mwriterOpts + unPandocLua $ getWriter formatSpec >>= \case + (TextWriter w, es) -> Right <$> + w writerOpts{ writerExtensions = es } doc + (ByteStringWriter w, es) -> Left <$> + w writerOpts{ writerExtensions = es } doc) + <#> parameter peekPandoc "Pandoc" "doc" "document to convert" + <#> opt (textParam "formatspec" "format and extensions") + <#> opt (parameter peekWriterOptions "WriterOptions" "writer_options" + "writer options") + =#> functionResult (either pushLazyByteString pushText) "string" + "result document" + ] + where + walkElement x f = + walkInlineSplicing f x + >>= walkInlinesStraight f + >>= walkBlockSplicing f + >>= walkBlocksStraight f + +data PipeError = PipeError + { pipeErrorCommand :: T.Text + , pipeErrorCode :: Int + , pipeErrorOutput :: BL.ByteString + } + +peekPipeError :: LuaError e => StackIndex -> LuaE e PipeError +peekPipeError idx = + PipeError + <$> (Lua.getfield idx "command" *> Lua.peek (-1) <* Lua.pop 1) + <*> (Lua.getfield idx "error_code" *> Lua.peek (-1) <* Lua.pop 1) + <*> (Lua.getfield idx "output" *> Lua.peek (-1) <* Lua.pop 1) + +pushPipeError :: LuaError e => Pusher e PipeError +pushPipeError pipeErr = do + pushAsTable [ ("command" , pushText . pipeErrorCommand) + , ("error_code" , pushIntegral . pipeErrorCode) + , ("output" , pushLazyByteString . pipeErrorOutput) + ] pipeErr + pushPipeErrorMetaTable + Lua.setmetatable (nth 2) + where + pushPipeErrorMetaTable :: LuaError e => LuaE e () + pushPipeErrorMetaTable = do + v <- Lua.newmetatable "pandoc pipe error" + when v $ do + pushName "__tostring" + pushHaskellFunction pipeErrorMessage + rawset (nth 3) + + pipeErrorMessage :: LuaError e => LuaE e NumResults + pipeErrorMessage = do + (PipeError cmd errorCode output) <- peekPipeError (nthBottom 1) + pushByteString . BSL.toStrict . BSL.concat $ + [ BSL.pack "Error running " + , BSL.pack $ T.unpack cmd + , BSL.pack " (error code " + , BSL.pack $ show errorCode + , BSL.pack "): " + , if output == mempty then BSL.pack "<no output>" else output + ] + return (NumResults 1) |
