{-# LANGUAGE OverloadedStrings #-}
-- | Module that communicates with GHCid, splits the commands in the code block,
-- gather the results and emits the trasnformed JSON based Pandoc AST.
module CodeBlockExecutor
(
applyFilterToBlock,
runCodeBlock,
processResults
) where
--import GHC.Generics
import Text.Pandoc
import Language.Haskell.Ghcid
import Control.Applicative
import Control.Exception
import Data.String
import Data.List as L
import Data.Map.Strict as M
import Data.Maybe
import qualified Data.Text as T
removeAll:: T.Text -> T.Text -> T.Text
removeAll pat str = if (T.replace pat "" str) == str then str
else removeAll pat (T.replace pat "" str)
-- | Determines if the command encountered is an `interactive` command.
-- This is one of the conditions that determines whether output needs to be captured.
isInteractive :: T.Text -> Bool
isInteractive cmd = T.isPrefixOf ">>" cmd
-- | This function takes care of placing back the newline characters
-- that may been stripped out before sending the commands to GHCid
updateSuffixForInteractiveCmd :: Data.String.IsString p => T.Text -> p
updateSuffixForInteractiveCmd cmd = if isInteractive cmd then
if T.last cmd == '\n' then "" else "\n"
else "\n\n"
-- | Intercalate the commands and respective results. Typically, only errors
-- encountered while running definitions, and outut of interactive commands (
-- i.e commands prefixed with `>>`) are captured. All empty strins are dropped.
intercalateCmdAndResults :: T.Text -> T.Text -> T.Text
intercalateCmdAndResults cmd result =
T.concat [cmd, updateSuffixForInteractiveCmd cmd, result, trailResult result] where
trailResult r = if r /= "" then "\n" else ""
-- | Post-processing function that interleaves command and results
processResults :: [T.Text] -- ^ List of commands that were executed
-> [T.Text] -- ^ List of results for the executed commands
-> String -- ^ New string that will form the bodyof the modified Code Block.
processResults cmds results =
let cmd_result = getZipList $ intercalateCmdAndResults <$> ZipList cmds <*> ZipList results
in
(T.unpack . T.concat) $ cmd_result
-- | Apply the filter block only if the language attribute
-- is set to `haskell` and `code-filter` property is *not* set to "Off"
--
-- Example:
-- ``` {.haskell code-filter="Off"} ``` will turn off any kind of transformation.
--
-- By default the filer is "On"
applyFilterToBlock:: Block -- ^ The 'Block' yielded by toJSONFilter in "Text.Pandoc.JSON"
-> IO Block -- ^ The newly minted 'Block'
applyFilterToBlock c@(CodeBlock (_, classes, key_values) _) = let
attrs = M.fromList key_values
haskell_in_class = L.find (== "haskell") classes
code_filter_flag = maybe "On" id (M.lookup ("code-filter") attrs)
in
if code_filter_flag == "On" && isJust haskell_in_class then runCodeBlock c
else (return c)
applyFilterToBlock b = return b
-- | Run the commands in the 'Block' in one single GHCid session.
runCodeBlock:: Block -- ^ 'Block' to execute. Only 'CodeBlock' is executed.
-> IO Block -- ^ transformed code block
runCodeBlock (CodeBlock attr str) = bracket startGhciProcess' stopGhci runCommands
where
startGhciProcess' = do
(ghci_handle, _) <- startGhci "stack ghci" (Just ".") (\_ _ -> return ())
return ghci_handle
runCommands g = do
let cmds = L.filter (\s -> s /= "") $ T.splitOn "\n\n" $ T.pack str
results <- mapM (runCmd g) cmds
let results''' = processResults cmds results
return (CodeBlock attr results''')
runCodeBlock b = return b
-- | Executes a command in GHCIid.
runCmd :: Ghci -- ^ Handle to the GHCi process through the GHCid interface
-> T.Text -- ^ Statement to execute
-> IO T.Text -- ^ Result of the executed statement
runCmd g cmd = do
let executeStatement = exec g
cmd_ = T.concat [":{\n", T.replace ">>" "" cmd, "\n:}\n"]
result <- executeStatement . T.unpack $ cmd_
-- we send this PROBE here since GHCi has its own mind on how it prefixes output based on its native needs. By sending the probe we can guess what is the latest prompt and then discard it while processing thye output.
probe <- exec g ":{\nshow (\"PANDOC_FILTER_PROBE_PROMPT_INTERNAL\"::String)\n:}\n"
let current_prompt = preparePrompt probe
where
preparePrompt probe' =
let prompt = T.replace " \"\\\"PANDOC_FILTER_PROBE_PROMPT_INTERNAL\\\"\"\n" "" (T.pack . unlines $ probe')
in
T.concat [T.takeWhile (/='|') prompt, "|"]
--putStrLn $ show . unlines $ probe
--putStrLn $ show current_prompt
result' = T.stripStart $ removeAll current_prompt (T.pack . unlines $ result)
--putStrLn $ show result'
return $ result'