1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
|
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{- |
Module : Text.Pandoc.Lua.Writer.Scaffolding
Copyright : © 2022-2024 Albert Krewinkel
License : GPL-2.0-or-later
Maintainer : Albert Krewinkel <[email protected]>
Conversion of Pandoc documents using a custom Lua writer.
-}
module Text.Pandoc.Lua.Writer.Scaffolding
( pushWriterScaffolding
) where
import Control.Monad ((<$!>), void)
import Data.ByteString (ByteString)
import Data.Data (dataTypeConstrs, dataTypeOf, showConstr, toConstr)
import Data.Default (def)
import Data.List (intersperse)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.String (IsString (fromString))
import HsLua
import HsLua.Module.DocLayout (peekDoc, pushDoc)
import Text.DocLayout (Doc, blankline, render)
import Text.DocTemplates (Context)
import Text.Pandoc.Definition
import Text.Pandoc.Error (PandocError (..))
import Text.Pandoc.Options (WriterOptions (..), WrapOption(..))
import Text.Pandoc.Lua.PandocLua ()
import Text.Pandoc.Lua.Marshal.AST
import Text.Pandoc.Lua.Marshal.Context (peekContext)
import Text.Pandoc.Lua.Marshal.WriterOptions ( peekWriterOptions
, pushWriterOptions)
import Text.Pandoc.Templates (renderTemplate)
import Text.Pandoc.Writers.Shared (metaToContext, setField)
import qualified Data.Text as T
import qualified Text.Pandoc.UTF8 as UTF8
-- | Convert Pandoc to custom markup.
pushWriterScaffolding :: LuaE PandocError NumResults
pushWriterScaffolding = do
newtable
*> pushWriterMT *> setmetatable (nth 2)
writer <- toWriterTable top
addField "Blocks" $ pushDocumentedFunction (blocksFn writer)
addField "Inlines" $ pushDocumentedFunction (inlinesFn writer)
addField "Block" $ newtable *> pushBlockMT writer *> setmetatable (nth 2)
addField "Inline" $ newtable *> pushInlineMT writer *> setmetatable (nth 2)
addField "Pandoc" $ pushDocumentedFunction $ lambda
### (\(Pandoc _ blks) -> do
pushWriterTable writer
getfield' top "Blocks"
pushBlocks blks
callTrace 1 1
pure (NumResults 1))
<#> parameter peekPandoc "Pandoc" "doc" ""
=?> "rendered doc"
freeWriter writer
return 1
where
blocksFn w = lambda
### (\blocks msep -> blockListToCustom w msep blocks)
<#> parameter peekBlocks "Blocks" "blocks" ""
<#> opt (parameter peekDocFuzzy "Doc" "sep" "")
=#> functionResult pushDoc "Doc" ""
inlinesFn w = lambda
### inlineListToCustom w
<#> parameter peekInlines "Inlines" "inlines" ""
=#> functionResult pushDoc "Doc" ""
pushBlockMT writer = do
newtable
addField "__call" $ pushDocumentedFunction $ lambda
### blockToCustom
<#> parameter peekWriter "table" "writer" ""
<#> parameter peekBlockFuzzy "Block" "block" ""
=#> functionResult pushDoc "Doc" "rendered blocks"
addField "__index" $
-- lookup missing fields in the main Writer table
pushWriterTable writer
pushInlineMT writer = do
newtable
addField "__call" $ pushDocumentedFunction $ lambda
### inlineToCustom
<#> parameter peekWriter "table" "writer" ""
<#> parameter peekInlineFuzzy "Inline" "inline" ""
=#> functionResult pushDoc "Doc" "rendered inline"
addField "__index" $ do
-- lookup missing fields in the main Writer table
pushWriterTable writer
pushWriterMT :: LuaE PandocError ()
pushWriterMT = do
newtable
addField "__call" $ pushDocumentedFunction $ lambda
### (\writer doc mopts -> runWriter writer doc mopts)
<#> parameter peekWriter "table" "writer" ""
<#> parameter peekPandoc "Pandoc" "doc" ""
<#> opt (parameter peekWriterOptions "WriterOptions" "opts" "")
=#> functionResult pushText "string" "rendered document"
addField "__index" . pushDocumentedFunction $ lambda
### (\_writer key -> handleMissingField key)
<#> parameter pure "table" "writer" ""
<#> parameter (liftLua . tostring') "string" "key" ""
=#> functionResult (const pushnil) "string" ""
addField :: LuaError e => Name -> LuaE e a -> LuaE e ()
addField name action = do
pushName name
action
rawset (nth 3)
getfield' :: LuaError e => StackIndex -> Name -> LuaE e HsLua.Type
getfield' idx name = do
aidx <- absindex idx
pushName name
rawget aidx >>= \case
TypeNil -> pop 1 *> getfield aidx name
ty -> pure ty
-- | A writer table is just an absolute stack index.
newtype WriterTable = WriterTable Reference
toWriterTable :: LuaError e => StackIndex -> LuaE e WriterTable
toWriterTable idx = WriterTable <$!> do
pushvalue idx
ref registryindex
peekWriter :: LuaError e => Peeker e WriterTable
peekWriter = liftLua . toWriterTable
pushWriterTable :: LuaError e => Pusher e WriterTable
pushWriterTable (WriterTable wref) = void $ getref registryindex wref
writerOptionsField :: Name
writerOptionsField = "Pandoc Writer WriterOptions"
freeWriter :: WriterTable -> LuaE e ()
freeWriter (WriterTable wref) = unref registryindex wref
pushOpts :: LuaE PandocError ()
pushOpts = void $ getfield' registryindex writerOptionsField
runWriter :: WriterTable -> Pandoc -> Maybe WriterOptions
-> LuaE PandocError Text
runWriter writer doc@(Pandoc meta _blks) mopts = do
let opts = fromMaybe def mopts
pushWriterOptions opts *>
setfield registryindex writerOptionsField
(body, mcontext) <- runPeek (pandocToCustom writer doc) >>= force . \case
Failure msg contexts -> Failure (cleanupTrace msg) contexts
s -> s
-- convert metavalues to a template context (variables)
defaultContext <- metaToContext opts
(blockListToCustom writer Nothing)
(inlineListToCustom writer)
meta
let context = setField "body" body
$ fromMaybe defaultContext mcontext
let colwidth = if writerWrapText opts == WrapAuto
then Just $ writerColumns opts
else Nothing
return $ render colwidth $
case writerTemplate opts of
Nothing -> body
Just tpl -> renderTemplate tpl context
-- | Keep exactly one traceback and clean it up. This wouldn't be
-- necessary if the @pcallTrace@ function would do nothing whenever the
-- error already included a trace, but that would require some bigger
-- changes; removing the additional traces in this post-process step is
-- much easier (for now).
cleanupTrace :: ByteString -> ByteString
cleanupTrace msg = UTF8.fromText . T.intercalate "\n" $
let tmsg = T.lines $ UTF8.toText msg
traceStart = (== "stack traceback:")
in case break traceStart tmsg of
(x, t:traces) -> (x <>) . (t:) $
let (firstTrace, rest) = break traceStart traces
isPeekContext = ("\twhile " `T.isPrefixOf`)
isUnknownCFn = (== "\t[C]: in ?")
in filter (not . isUnknownCFn) firstTrace <>
filter isPeekContext rest
_ -> tmsg
-- | Pushes the field in the writer table.
getWriterField :: LuaError e
=> WriterTable -> Name -> LuaE e HsLua.Type
getWriterField writer name = do
pushWriterTable writer
getfield' top name <* remove (nth 2)
-- | Looks up @Writer.subtable.field@; tries @Writer.field@ as a fallback if the
-- subtable field is @nil@.
getNestedWriterField :: LuaError e
=> WriterTable -> Name -> Name -> LuaE e HsLua.Type
getNestedWriterField writer subtable field = do
pushWriterTable writer
getfield' top subtable >>= \case
TypeNil -> TypeNil <$ remove (nth 2) -- remove Writer table
_ -> getfield' top field
-- remove Writer and subtable
<* remove (nth 3) <* remove (nth 2)
pandocToCustom :: WriterTable -> Pandoc
-> Peek PandocError (Doc Text, Maybe (Context Text))
pandocToCustom writer doc = withContext "rendering Pandoc" $ do
callStatus <- liftLua $ do
getWriterField writer "Pandoc"
pushPandoc doc
pushOpts
pcallTrace 2 2
case callStatus of
OK -> ((,) <$> peekDocFuzzy (nth 2) <*> orNil peekContext top)
`lastly` pop 2
_ -> failPeek =<< liftLua (tostring' top)
blockToCustom :: WriterTable -> Block -> LuaE PandocError (Doc Text)
blockToCustom writer blk = forcePeek $ renderBlock writer blk
renderBlock :: WriterTable -> Block -> Peek PandocError (Doc Text)
renderBlock writer blk = do
let constrName = fromString . showConstr . toConstr $ blk
withContext ("rendering Block `" <> constrName <> "`") $
liftLua (getNestedWriterField writer "Block" constrName) >>= \case
TypeNil -> failPeek =<< typeMismatchMessage "function or Doc" top
_ -> callOrDoc (pushBlock blk)
inlineToCustom :: WriterTable -> Inline -> LuaE PandocError (Doc Text)
inlineToCustom writer inln = forcePeek $ renderInline writer inln
renderInline :: WriterTable -> Inline -> Peek PandocError (Doc Text)
renderInline writer inln = do
let constrName = fromString . showConstr . toConstr $ inln
withContext ("rendering Inline `" <> constrName <> "`") $ do
liftLua (getNestedWriterField writer "Inline" constrName) >>= \case
TypeNil -> failPeek =<< typeMismatchMessage "function or Doc" top
_ -> callOrDoc (pushInline inln)
-- | If the value at the top of the stack can be called as a function,
-- then push the element and writer options to the stack and call it;
-- otherwise treat it as a plain Doc value
callOrDoc :: LuaE PandocError ()
-> Peek PandocError (Doc Text)
callOrDoc pushElement = do
liftLua (ltype top) >>= \case
TypeFunction -> peekCall
_ ->
liftLua (getmetafield top "__call") >>= \case
TypeNil -> peekDocFuzzy top
_ -> liftLua (pop 1) *> peekCall
where
peekCall :: Peek PandocError (Doc Text)
peekCall =
liftLua (pushElement *> pushOpts *> pcallTrace 2 1) >>= \case
OK -> peekDocFuzzy top
_ -> failPeek =<< liftLua (tostring' top)
blockListToCustom :: WriterTable -> Maybe (Doc Text) -> [Block]
-> LuaE PandocError (Doc Text)
blockListToCustom writer msep blocks = forcePeek $
renderBlockList writer msep blocks
inlineListToCustom :: WriterTable -> [Inline] -> LuaE PandocError (Doc Text)
inlineListToCustom writer inlines = forcePeek $
renderInlineList writer inlines
renderBlockList :: WriterTable -> Maybe (Doc Text) -> [Block]
-> Peek PandocError (Doc Text)
renderBlockList writer msep blocks = withContext "rendering Blocks" $ do
let addSeps = intersperse $ fromMaybe blankline msep
mconcat . addSeps <$> mapM (renderBlock writer) blocks
renderInlineList :: WriterTable -> [Inline] -> Peek PandocError (Doc Text)
renderInlineList writer inlines = withContext "rendering Inlines" $ do
mconcat <$> mapM (renderInline writer) inlines
orNil :: Peeker e a -> Peeker e (Maybe a)
orNil p idx = liftLua (ltype idx) >>= \case
TypeNil -> pure Nothing
TypeNone -> pure Nothing
_ -> Just <$> p idx
peekDocFuzzy :: LuaError e => Peeker e (Doc Text)
peekDocFuzzy idx = liftLua (ltype idx) >>= \case
TypeTable -> mconcat <$!> peekList peekDoc idx
_ -> peekDoc idx
handleMissingField :: LuaError e => ByteString -> LuaE e ()
handleMissingField key' =
let key = UTF8.toString key'
blockNames = map (fromString . show) . dataTypeConstrs . dataTypeOf
$ HorizontalRule
inlineNames = map (fromString . show) . dataTypeConstrs . dataTypeOf
$ Space
mtypeName = case () of
_ | key `elem` blockNames -> Just "Block"
_ | key `elem` inlineNames -> Just "Inline"
_ -> Nothing
in case mtypeName of
Just typeName -> failLua $
"No render function for " <> typeName <> " value " <>
"'" <> key <> "';\ndefine a function `Writer." <>
typeName <> "." <> key <> "` that returns " <>
"a string or Doc."
_ -> pure ()
|