aboutsummaryrefslogtreecommitdiff
path: root/pandoc-lua-engine/src/Text/Pandoc/Lua/Reader.hs
blob: 2c2dd369b70487a4dd603a99a000c4e5f5b19f24 (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
{-# LANGUAGE LambdaCase          #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE TypeApplications    #-}
{-# LANGUAGE TupleSections       #-}
{- |
   Module      : Text.Pandoc.Lua.Reader
   Copyright   : Copyright (C) 2021-2022 John MacFarlane
   License     : GNU GPL, version 2 or above

   Maintainer  : John MacFarlane <[email protected]>
   Stability   : alpha
   Portability : portable

Supports custom parsers written in Lua which produce a Pandoc AST.
-}
module Text.Pandoc.Lua.Reader ( readCustom ) where
import Control.Exception
import Control.Monad ((<=<), (<$!>), when)
import Control.Monad.IO.Class (MonadIO)
import Data.Maybe (fromMaybe)
import HsLua as Lua hiding (Operation (Div))
import HsLua.Core.Run (GCManagedState, newGCManagedState, withGCManagedState)
import Text.Pandoc.Class (PandocMonad, findFileWithDataFallback, report)
import Text.Pandoc.Error (PandocError)
import Text.Pandoc.Format (ExtensionsConfig (..))
import Text.Pandoc.Logging
import Text.Pandoc.Lua.Global (Global (..), setGlobals)
import Text.Pandoc.Lua.Init (runLuaWith)
import Text.Pandoc.Lua.PandocLua
import Text.Pandoc.Lua.Marshal.Format (peekExtensionsConfig)
import Text.Pandoc.Lua.Marshal.Pandoc (peekPandoc)
import Text.Pandoc.Readers (Reader (..))
import Text.Pandoc.Sources (ToSources(..), sourcesToText)
import qualified Data.Text as T

-- | Convert custom markup to Pandoc.
readCustom :: (PandocMonad m, MonadIO m)
            => FilePath -> m (Reader m, ExtensionsConfig)
readCustom luaFile = do
  luaState <- liftIO newGCManagedState
  luaFile' <- fromMaybe luaFile <$> findFileWithDataFallback "readers" luaFile
  either throw pure <=< runLuaWith luaState $ do
    let globals = [ PANDOC_SCRIPT_FILE luaFile ]
    setGlobals globals
    stat <- dofileTrace luaFile'
    -- check for error in lua script (later we'll change the return type
    -- to handle this more gracefully):
    when (stat /= Lua.OK)
      Lua.throwErrorAsException

    extsConf <- getglobal "reader_extensions" >>= \case
      TypeNil   -> pure $ ExtensionsConfig mempty mempty
      _         -> forcePeek $ peekExtensionsConfig top `lastly` pop 1

    (,extsConf) <$!> getCustomReader luaState

 where
  readerField = "PANDOC Reader function"
  inLua st = liftIO . withGCManagedState @PandocError st
  byteStringReader :: MonadIO m => GCManagedState -> Reader m
  byteStringReader st = ByteStringReader $ \ropts input -> inLua st $ do
    getfield registryindex readerField
    push input
    push ropts
    callTrace 2 1
    forcePeek $ peekPandoc top
  textReader st = TextReader $ \ropts srcs -> inLua st $ do
    let input = toSources srcs
    getfield registryindex readerField
    push input
    push ropts
    pcallTrace 2 1 >>= \case
      OK -> forcePeek $ peekPandoc top
      ErrRun -> do
        -- Caught a runtime error. Check if parsing might work if we
        -- pass a string instead of a Sources list, then retry.
        runPeek (peekText top) >>= \case
          Failure {} ->
            -- not a string error object. Bail!
            throwErrorAsException
          Success errmsg ->
            if "string expected, got pandoc Sources" `T.isInfixOf` errmsg
            then do
              pop 1
              _ <- unPandocLua $ do
                report $ Deprecated "old Reader function signature" $
                  T.unlines
                  [ "Reader functions should accept a sources list; "
                  , "functions expecting `string` input are deprecated. "
                  , "Use `tostring` to convert the first argument to a "
                  , "string."
                  ]
              getglobal "Reader"
              push $ sourcesToText input  -- push sources as string
              push ropts
              callTrace 2 1
              forcePeek $ peekPandoc top
            else
              -- nothing we can do here
              throwErrorAsException
      _ ->  -- not a runtime error, we won't be able to recover from that
              throwErrorAsException
  getCustomReader st = do
    getglobal "Reader" >>= \case
      TypeNil -> do
        pop 1
        getglobal "ByteStringReader" >>= \case
          TypeNil -> failLua $ "No reader function found: either 'Reader' or "
                     <> "'ByteStringReader' must be defined."
          _ -> do
            setfield registryindex readerField
            pure (byteStringReader st)
      _ -> do
        setfield registryindex readerField
        pure (textReader st)