Skip to content

Commit 1b54b43

Browse files
committed
Lua: add function pandoc.utils.documentation
Closes: #10999
1 parent 8af4ede commit 1b54b43

File tree

6 files changed

+206
-7
lines changed

6 files changed

+206
-7
lines changed

pandoc-lua-engine/pandoc-lua-engine.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ library
6969
hs-source-dirs: src
7070
exposed-modules: Text.Pandoc.Lua
7171
other-modules: Text.Pandoc.Lua.Custom
72+
, Text.Pandoc.Lua.Documentation
7273
, Text.Pandoc.Lua.Engine
7374
, Text.Pandoc.Lua.Filter
7475
, Text.Pandoc.Lua.Global
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
{-# LANGUAGE LambdaCase #-}
2+
{-# LANGUAGE OverloadedStrings #-}
3+
{-# LANGUAGE RankNTypes #-}
4+
{- |
5+
Module : Text.Pandoc.Lua.Documentation
6+
Copyright : Copyright © 2026 Albert Krewinkel
7+
License : GPL-2.0-or-later
8+
Maintainer : Albert Krewinkel <albert+pandoc@tarleb.com>
9+
10+
Render Lua documentation
11+
-}
12+
module Text.Pandoc.Lua.Documentation
13+
( renderDocumentation
14+
) where
15+
16+
import Data.Default (def)
17+
import Data.List (intersperse)
18+
import Data.Sequence (Seq ((:|>)))
19+
import Data.Version (showVersion)
20+
import HsLua as Lua
21+
import Text.Pandoc.Class (runPure)
22+
import Text.Pandoc.Definition (Pandoc (Pandoc))
23+
import Text.Pandoc.Extensions (extensionsFromList, Extension (..))
24+
import Text.Pandoc.Options (ReaderOptions (readerExtensions))
25+
import Text.Pandoc.Readers (readCommonMark)
26+
import Text.Pandoc.Walk (walk)
27+
28+
import qualified Data.Text as T
29+
import qualified Text.Pandoc.Builder as B
30+
import qualified Text.Pandoc.UTF8 as UTF8
31+
32+
-- | Render the documentation object as pandoc Blocks
33+
renderDocumentation :: DocumentationObject -> B.Blocks
34+
renderDocumentation = \case
35+
DocObjectFunction fn -> renderFunctionDoc Nothing fn
36+
DocObjectModule mdl -> renderModuleDoc mdl
37+
DocObjectType tp -> renderTypeDoc tp
38+
39+
renderTypeDoc :: TypeDoc -> B.Blocks
40+
renderTypeDoc td = mconcat
41+
[ B.header 1 (B.str $ typeDocName td)
42+
, parseCommonMark $ typeDocDescription td
43+
, if null $ typeDocMethods td
44+
then mempty
45+
else
46+
B.header 2 "Methods" <>
47+
(shiftHeadings 2 . mconcat . map (renderFunctionDoc Nothing) $
48+
typeDocMethods td)
49+
]
50+
51+
-- Shift headings
52+
shiftHeadings :: Int -> B.Blocks -> B.Blocks
53+
shiftHeadings incr blks = flip walk blks $ \case
54+
B.Header level attr inner -> B.Header (level + incr) attr inner
55+
x -> x
56+
57+
renderModuleDoc :: ModuleDoc -> B.Blocks
58+
renderModuleDoc moddoc = mconcat
59+
[ B.headerWith ("module-" <> moduleDocName moddoc, [], []) 1
60+
(B.str $ "Module " <> moduleDocName moddoc)
61+
, parseCommonMark (moduleDocDescription moddoc)
62+
, if null (moduleDocFields moddoc)
63+
then mempty
64+
else
65+
let ident = moduleDocName moddoc <> "-fields"
66+
in B.headerWith (ident, [], []) 2 (B.str "Fields") <>
67+
shiftHeadings 0 (mconcat (map renderFieldDoc (moduleDocFields moddoc)))
68+
, if null (moduleDocFunctions moddoc)
69+
then mempty
70+
else
71+
let ident = moduleDocName moddoc <> "-functions"
72+
in B.headerWith (ident, [], []) 2 (B.str "Functions") <>
73+
(shiftHeadings 2 . mconcat . map (renderFunctionDoc Nothing) $
74+
moduleDocFunctions moddoc)
75+
, if null (moduleDocTypes moddoc)
76+
then mempty
77+
else
78+
let ident = moduleDocName moddoc <> "-types"
79+
in B.headerWith (ident, [], []) 2 (B.str "Types") <>
80+
(shiftHeadings 2 . mconcat . map renderTypeDoc . reverse $
81+
moduleDocTypes moddoc)
82+
]
83+
84+
parseCommonMark :: T.Text -> B.Blocks
85+
parseCommonMark txt =
86+
let exts = extensionsFromList
87+
[ Ext_wikilinks_title_after_pipe
88+
, Ext_smart
89+
]
90+
result = runPure $ do
91+
Pandoc _ blks <- readCommonMark (def {readerExtensions = exts}) txt
92+
return $ B.fromList blks
93+
in either mempty id result
94+
95+
appendInlines :: B.Blocks -> B.Inlines -> B.Blocks
96+
appendInlines blks inlns = case B.unMany blks of
97+
front :|> (B.Para xs) -> B.Many front <> B.para (addTo xs)
98+
front :|> (B.Plain xs) -> B.Many front <> B.plain (addTo xs)
99+
_ -> blks <> B.para inlns
100+
where addTo xs = B.fromList xs <> B.space <> inlns
101+
102+
appendType :: B.Blocks -> TypeSpec -> B.Blocks
103+
appendType blks typespec =
104+
appendInlines blks (B.str "(" <> typeToInlines typespec <> B.str ")")
105+
106+
typeToInlines :: TypeSpec -> B.Inlines
107+
typeToInlines = \case
108+
bt@BasicType{} -> builtin $ tystr bt
109+
NamedType "integer" -> builtin "integer"
110+
NamedType name -> B.link ("#" <> n2t name) mempty $ B.str (n2t name)
111+
SeqType itemtype -> "{" <> typeToInlines itemtype <> ",...}"
112+
SumType summands -> mconcat . intersperse (B.str "|") $ map typeToInlines summands
113+
AnyType -> "any"
114+
x -> tystr x
115+
where
116+
tystr = B.str . T.pack . typeSpecToString
117+
n2t = UTF8.toText . fromName
118+
builtin = B.spanWith ("", ["builtin-lua-type"], [])
119+
120+
renderFunctionDoc :: Maybe T.Text -> FunctionDoc -> B.Blocks
121+
renderFunctionDoc mbmodule fndoc =
122+
let name = funDocName fndoc
123+
level = 1
124+
ident = maybe "" (<> ".") mbmodule <> name
125+
argsString = argslist (funDocParameters fndoc)
126+
paramToDefItem p = ( B.code $ parameterName p
127+
, [ appendType
128+
(parseCommonMark $ parameterDescription p)
129+
(parameterType p)
130+
]
131+
)
132+
paramlist = B.definitionList $ map paramToDefItem $ funDocParameters fndoc
133+
in mconcat
134+
[ B.headerWith (ident, [], []) level (B.str name)
135+
, B.plain (B.code $ name <> " (" <> argsString <> ")")
136+
, parseCommonMark (funDocDescription fndoc)
137+
, if null (funDocParameters fndoc)
138+
then mempty
139+
else B.para "Parameters:" <> paramlist
140+
, if funDocResults fndoc == ResultsDocList []
141+
then mempty
142+
else B.para "Returns:" <> renderResults (funDocResults fndoc)
143+
, case funDocSince fndoc of
144+
Nothing -> mempty
145+
Just version ->
146+
B.para $ B.emph $ "Since: " <> (B.str . T.pack $ showVersion version)
147+
]
148+
149+
renderResults :: ResultsDoc -> B.Blocks
150+
renderResults (ResultsDocMult descr) = parseCommonMark descr
151+
renderResults (ResultsDocList rvd) = B.bulletList $ map renderResultVal rvd
152+
where
153+
renderResultVal (ResultValueDoc typespec descr) =
154+
parseCommonMark descr `appendType` typespec
155+
156+
argslist :: [ParameterDoc] -> T.Text
157+
argslist params =
158+
-- Expect optional values to come after required values.
159+
let (required, optional') = break parameterIsOptional params
160+
reqs = map parameterName required
161+
opts = map parameterName optional'
162+
in if null opts
163+
then T.intercalate ", " reqs
164+
else T.intercalate ", " reqs <>
165+
(if null required then "[" else "[, ") <>
166+
T.intercalate "[, " opts <> T.replicate (length opts) "]"
167+
168+
renderFieldDoc :: FieldDoc -> B.Blocks
169+
renderFieldDoc fd =
170+
B.headerWith (fieldDocName fd, [], []) 3 (B.str (fieldDocName fd)) <>
171+
appendType (parseCommonMark $ fieldDocDescription fd) (fieldDocType fd)

pandoc-lua-engine/src/Text/Pandoc/Lua/Init.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
{-# LANGUAGE RankNTypes #-}
44
{- |
55
Module : Text.Pandoc.Lua.Init
6-
Copyright : © 2017-2024 Albert Krewinkel
6+
Copyright : © 2017-2026 Albert Krewinkel
77
License : GPL-2.0-or-later
88
Maintainer : Albert Krewinkel <albert+pandoc@tarleb.com>
99

pandoc-lua-engine/src/Text/Pandoc/Lua/Module.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ submodules =
9696
`allSince` [2,18])
9797
`functionsSince` ["bold", "italic", "underlined", "strikeout", "fg", "bg"])
9898
[3, 4, 1]
99-
, Module.Zip.documentedModule { moduleName = "pandoc.zip" }
99+
, Module.Zip.documentedModule `renameTo` "pandoc.zip"
100100
`allSince` [3,0]
101101
]
102102
where

pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Path.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
{-# LANGUAGE TypeApplications #-}
44
{- |
55
Module : Text.Pandoc.Lua.Module.Path
6-
Copyright : © 2019-2024 Albert Krewinkel
6+
Copyright : © 2019-2026 Albert Krewinkel
77
License : GNU GPL, version 2 or above
88
99
Maintainer : Albert Krewinkel <albert+pandoc@tarleb.com>

pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Utils.hs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
{-# LANGUAGE TypeApplications #-}
55
{- |
66
Module : Text.Pandoc.Lua.Module.Utils
7-
Copyright : Copyright © 2017-2026 Albert Krewinkel
7+
Copyright : © 2017-2026 Albert Krewinkel
88
License : GNU GPL, version 2 or above
99
1010
Maintainer : Albert Krewinkel <albert+pandoc@tarleb.com>
@@ -19,6 +19,7 @@ module Text.Pandoc.Lua.Module.Utils
1919

2020
import Control.Applicative ((<|>))
2121
import Control.Monad ((<$!>))
22+
import Control.Monad.Except (MonadError (throwError))
2223
import Crypto.Hash (hashWith, SHA1(SHA1))
2324
import Data.Data (showConstr, toConstr)
2425
import Data.Default (def)
@@ -28,12 +29,17 @@ import HsLua as Lua
2829
import HsLua.Module.Version (peekVersionFuzzy, pushVersion)
2930
import Text.Pandoc.Citeproc (getReferences, processCitations)
3031
import Text.Pandoc.Definition
31-
import Text.Pandoc.Error (PandocError)
32+
import Text.Pandoc.Error (PandocError (PandocLuaError))
3233
import Text.Pandoc.Filter (applyJSONFilter)
34+
import Text.Pandoc.Format (FlavoredFormat (formatName), parseFlavoredFormat)
35+
import Text.Pandoc.Lua.Documentation (renderDocumentation)
3336
import Text.Pandoc.Lua.Filter (runFilterFile')
3437
import Text.Pandoc.Lua.Marshal.AST
38+
import Text.Pandoc.Lua.Marshal.Format (peekFlavoredFormat)
3539
import Text.Pandoc.Lua.Marshal.Reference
3640
import Text.Pandoc.Lua.PandocLua (PandocLua (unPandocLua))
41+
import Text.Pandoc.Options (WriterOptions (writerExtensions))
42+
import Text.Pandoc.Writers (Writer (..), getWriter)
3743

3844
import qualified Data.Map as Map
3945
import qualified Data.Text as T
@@ -52,6 +58,7 @@ documentedModule = defmodule "pandoc.utils"
5258
`withFunctions`
5359
[ blocks_to_inlines `since` v[2,2,3]
5460
, citeproc `since` v[2,19,1]
61+
, documentation `since` v[3,8,4]
5562
, equals `since` v[2,5]
5663
, from_simple_table `since` v[2,11]
5764
, make_sections `since` v[2,8]
@@ -67,8 +74,7 @@ documentedModule = defmodule "pandoc.utils"
6774

6875
, defun "Version"
6976
### liftPure (id @Version)
70-
<#> parameter peekVersionFuzzy
71-
"version string, list of integers, or integer"
77+
<#> parameter peekVersionFuzzy "{Version,string,{integer,...},number}"
7278
"v" "version description"
7379
=#> functionResult pushVersion "Version" "new Version object"
7480
#? "Creates a Version object."
@@ -125,6 +131,27 @@ citeproc = defun "citeproc"
125131
, " end"
126132
]
127133

134+
documentation :: DocumentedFunction PandocError
135+
documentation = defun "documentation"
136+
### (\idx mformat -> do
137+
docobj <- getdocumentation idx >>= \case
138+
TypeNil -> fail "Undocumented object"
139+
_ -> forcePeek $ peekDocumentationObject top
140+
let blocks = renderDocumentation docobj
141+
if maybe mempty formatName mformat == "blocks"
142+
then pure . Left $ B.toList blocks
143+
else unPandocLua $ do
144+
flvrd <- maybe (parseFlavoredFormat "ansi") pure mformat
145+
getWriter flvrd >>= \case
146+
(TextWriter w, es) -> Right <$>
147+
w def{ writerExtensions = es } (B.doc blocks)
148+
_ -> throwError $ PandocLuaError
149+
"ByteString writers are not supported here.")
150+
<#> parameter pure "any" "object" "Retrieve documentation for this object"
151+
<#> opt (parameter peekFlavoredFormat "string|table" "format" "result format")
152+
=#> functionResult (either pushBlocks pushText) "string|Blocks"
153+
"rendered documentation"
154+
128155
equals :: LuaError e => DocumentedFunction e
129156
equals = defun "equals"
130157
### equal

0 commit comments

Comments
 (0)