aboutsummaryrefslogtreecommitdiff
path: root/pandoc-lua-engine/src/Text/Pandoc/Lua/Writer/Scaffolding.hs
blob: 300c1ee61e04ca92127ee6c4c349d7b43e26c3ae (plain)
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 ()