{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE Unsafe #-}
module Text.Pandoc.Filter.Pyplot (
makePlot
, makePlot'
, plotTransform
, PandocPyplotError(..)
, showError
) where
import Control.Monad ((>=>))
import qualified Data.Map.Strict as M
import System.Directory (doesDirectoryExist)
import System.FilePath (isValid, replaceExtension, takeDirectory)
import Text.Pandoc.Definition
import Text.Pandoc.Walk (walkM)
import Text.Pandoc.Filter.Scripting
data PandocPyplotError = ScriptError Int
| InvalidTargetError FilePath
| MissingDirectoryError FilePath
| BlockingCallError
data FigureSpec = FigureSpec
{ target :: FilePath
, alt :: String
, script :: PythonScript
, blockAttrs :: Attr
}
scriptWithCapture :: FigureSpec -> PythonScript
scriptWithCapture spec = addPlotCapture (target spec) (script spec)
scriptSourcePath :: FigureSpec -> FilePath
scriptSourcePath spec = replaceExtension (target spec) ".txt"
presentableScript :: FigureSpec -> PythonScript
presentableScript spec = mconcat [ "# Source code for ", target spec, "\n", script spec ]
targetKey, altTextKey :: String
targetKey = "plot_target"
altTextKey = "plot_alt"
parseFigureSpec :: Block -> Maybe FigureSpec
parseFigureSpec (CodeBlock (id', cls, attrs) content) =
createInclusion <$> M.lookup targetKey attrs'
where
attrs' = M.fromList attrs
inclusionKeys = [ targetKey, altTextKey ]
filteredAttrs = filter (\(k,_) -> k `notElem` inclusionKeys) attrs
createInclusion fname = FigureSpec
{ target = fname
, alt = M.findWithDefault "Figure generated by pandoc-pyplot" altTextKey attrs'
, script = content
, blockAttrs = (id', cls, filteredAttrs)
}
parseFigureSpec _ = Nothing
makePlot' :: Block -> IO (Either PandocPyplotError Block)
makePlot' block =
case parseFigureSpec block of
Nothing -> return $ Right block
Just spec -> do
let figurePath = target spec
figureDir = takeDirectory figurePath
scriptSource = script spec
validDirectory <- doesDirectoryExist figureDir
if | not (isValid figurePath) -> return $ Left $ InvalidTargetError figurePath
| not validDirectory -> return $ Left $ MissingDirectoryError figureDir
| hasBlockingShowCall scriptSource -> return $ Left $ BlockingCallError
| otherwise -> do
result <- runTempPythonScript (scriptWithCapture spec)
case result of
ScriptFailure code -> return $ Left $ ScriptError code
ScriptSuccess -> do
let sourcePath = scriptSourcePath spec
writeFile sourcePath (presentableScript spec)
let relevantAttrs = blockAttrs spec
srcTarget = Link nullAttr [Str "Source code"] (sourcePath, "")
caption' = [Str $ alt spec, Space, Str "(", srcTarget, Str ")"]
image = Image relevantAttrs caption' (figurePath, "fig:")
return $ Right $ Para $ [image]
showError :: PandocPyplotError -> String
showError (ScriptError exitcode) = "Script error: plot could not be generated. Exit code " <> (show exitcode)
showError (InvalidTargetError fname) = "Target filename " <> fname <> " is not valid."
showError (MissingDirectoryError dirname) = "Target directory " <> dirname <> " does not exist."
showError BlockingCallError = "Script contains a blocking call to show, like 'plt.show()'"
makePlot :: Block -> IO Block
makePlot = makePlot' >=> either (fail . showError) return
plotTransform :: Pandoc -> IO Pandoc
plotTransform = walkM makePlot