{-# LANGUAGE OverloadedStrings #-} {-| Module : Foreign.Lua.Module Copyright : © 2019-2020 Albert Krewinkel License : MIT Maintainer : Albert Krewinkel Stability : alpha Portability : Requires GHC 8 or later. Utility functions for HsLua modules. -} module Foreign.Lua.Module ( requirehs , preloadhs , addfield , addfunction , create -- * Module , Module (..) , Field (..) , registerModule , preloadModule , pushModule -- * Documentation , render ) where import Control.Monad (unless, forM_) import Data.Text (Text) import Foreign.Lua.Call (HaskellFunction) import Foreign.Lua.Core import Foreign.Lua.Push (pushText) import Foreign.Lua.Types (Pushable, push) import Foreign.Lua.FunctionCalling ( ToHaskellFunction , pushHaskellFunction ) import qualified Data.Text as T import qualified Foreign.Lua.Call as Call -- | Load a module, defined by a Haskell action, under the given -- name. -- -- Similar to @luaL_required@: After checking "loaded" table, -- calls @pushMod@ to push a module to the stack, and registers -- the result in @package.loaded@ table. -- -- The @pushMod@ function must push exactly one element to the top -- of the stack. This is not checked, but failure to do so will -- lead to problems. Lua's @package@ module must have been loaded -- by the time this function is invoked. -- -- Leaves a copy of the module on the stack. requirehs :: String -> Lua () -> Lua () requirehs modname pushMod = do -- get table of loaded modules getfield registryindex loadedTableRegistryField -- Check whether module has already been loaded. getfield stackTop modname -- LOADED[modname] alreadyLoaded <- toboolean stackTop unless alreadyLoaded $ do pop 1 -- remove field pushMod -- push module pushvalue stackTop -- make copy of module -- add module under the given name (LOADED[modname] = module) setfield (nthFromTop 3) modname remove (nthFromTop 2) -- remove table of loaded modules -- | Registers a preloading function. Takes an module name and the -- Lua operation which produces the package. preloadhs :: String -> Lua NumResults -> Lua () preloadhs name pushMod = do getfield registryindex preloadTableRegistryField pushHaskellFunction pushMod setfield (nthFromTop 2) name pop 1 -- | Add a string-indexed field to the table at the top of the -- stack. addfield :: Pushable a => String -> a -> Lua () addfield name value = do push name push value rawset (nthFromTop 3) -- | Attach a function to the table at the top of the stack, using -- the given name. addfunction :: ToHaskellFunction a => String -> a -> Lua () addfunction name fn = do push name pushHaskellFunction fn rawset (nthFromTop 3) -- | Create a new module (i.e., a Lua table). create :: Lua () create = newtable -- | Named and documented Lua module. data Module = Module { moduleName :: Text , moduleDescription :: Text , moduleFields :: [Field] , moduleFunctions :: [(Text, HaskellFunction)] } -- | Self-documenting module field data Field = Field { fieldName :: Text , fieldDescription :: Text , fieldPushValue :: Lua () } -- | Registers a 'Module'; leaves a copy of the module table on -- the stack. registerModule :: Module -> Lua () registerModule mdl = requirehs (T.unpack $ moduleName mdl) (pushModule mdl) -- | Preload self-documenting module. preloadModule :: Module -> Lua () preloadModule mdl = preloadhs (T.unpack $ moduleName mdl) $ do pushModule mdl return (NumResults 1) pushModule :: Module -> Lua () pushModule mdl = do create forM_ (moduleFunctions mdl) $ \(name, fn) -> do pushText name Call.pushHaskellFunction fn rawset (nthFromTop 3) -- | Renders module documentation as Markdown. render :: Module -> Text render mdl = let fields = moduleFields mdl in T.unlines [ "# " <> moduleName mdl , "" , moduleDescription mdl , if null (moduleFields mdl) then "" else renderFields fields , "## Functions" , "" ] <> T.intercalate "\n" (map (uncurry renderFunctionDoc) (moduleFunctions mdl)) -- | Renders documentation of a function. renderFunctionDoc :: Text -- ^ name -> HaskellFunction -- ^ function -> Text -- ^ function docs renderFunctionDoc name fn = case Call.functionDoc fn of Nothing -> "" Just fnDoc -> T.intercalate "\n" [ "### " <> name <> " (" <> renderFunctionParams fnDoc <> ")" , "" , Call.render fnDoc ] renderFunctionParams :: Call.FunctionDoc -> Text renderFunctionParams fd = T.intercalate ", " . map Call.parameterName $ Call.parameterDocs fd -- | Render documentation for fields as Markdown. renderFields :: [Field] -> Text renderFields fs = if null fs then mempty else T.unlines $ map renderField fs -- | Renders documentation for a single field. renderField :: Field -> Text renderField f = "### " <> fieldName f <> "\n\n" <> fieldDescription f <> "\n"