{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE Unsafe #-} {-| Module : Text.Pandoc.Filter.Pyplot Description : Pandoc filter to create Matplotlib figures from code blocks Copyright : (c) Laurent P René de Cotret, 2018 License : MIT Maintainer : laurent.decotret@outlook.com Stability : stable Portability : portable This module defines a Pandoc filter @makePlot@ that can be used to walk over a Pandoc document and generate figures from Python code blocks. -} module Text.Pandoc.Filter.Pyplot ( makePlot , makePlot' , PandocPyplotError(..) , showError ) where import Control.Monad ((>=>)) import qualified Data.Map.Strict as M import System.FilePath (replaceExtension, isValid) import Text.Pandoc.Definition import Text.Pandoc.Filter.Scripting -- | Possible errors returned by the filter data PandocPyplotError = ScriptError Int -- ^ Running Python script has yielded an error | InvalidTargetError FilePath -- ^ Invalid figure path | BlockingCallError -- ^ Python script contains a block call to 'show()' -- | Datatype containing all parameters required -- to run pandoc-pyplot data FigureSpec = FigureSpec { target :: FilePath -- ^ filepath where generated figure will be saved , alt :: String -- ^ Alternate text for the figure (optional) , caption :: String -- ^ Figure caption (optional) } -- Keys that pandoc-pyplot will look for in code blocks targetKey, altTextKey, captionKey :: String targetKey = "plot_target" altTextKey = "plot_alt" captionKey = "plot_caption" -- | Determine inclusion specifications from Block attributes. -- Note that the target key is required, but all other parameters are optional parseFigureSpec :: M.Map String String -> Maybe FigureSpec parseFigureSpec attrs = createInclusion <$> M.lookup targetKey attrs where defaultAltText = "Figure generated by pandoc-pyplot" defaultCaption = mempty createInclusion fname = FigureSpec { target = fname , alt = M.findWithDefault defaultAltText altTextKey attrs , caption = M.findWithDefault defaultCaption captionKey attrs } -- | Format the script source based on figure spec. formatScriptSource :: FigureSpec -> PythonScript -> PythonScript formatScriptSource spec script = mconcat [ "# Source code for " <> target spec , "\n" , script ] -- | Main routine to include Matplotlib plots. -- Code blocks containing the attributes @plot_target@ are considered -- Python plotting scripts. All other possible blocks are ignored. -- The source code is also saved in another file, which can be access by -- clicking the image makePlot' :: Block -> IO (Either PandocPyplotError Block) makePlot' cb @ (CodeBlock (id', cls, attrs) scriptSource) = case parseFigureSpec (M.fromList attrs) of -- Could not parse - leave code block unchanged Nothing -> return $ Right cb -- Could parse : run the script and capture output Just spec -> do let figurePath = target spec if | not (isValid figurePath) -> return $ Left $ InvalidTargetError figurePath | hasBlockingShowCall scriptSource -> return $ Left $ BlockingCallError | otherwise -> do script <- addPlotCapture figurePath scriptSource result <- runTempPythonScript script case result of ScriptFailure code -> return $ Left $ ScriptError code ScriptSuccess -> do -- Save the original script into a separate file -- so it can be inspected -- Note : using a .txt file allows to view source directly -- in the browser, in the case of HTML output let sourcePath = replaceExtension figurePath ".txt" writeFile sourcePath $ formatScriptSource spec scriptSource -- Propagate attributes that are not related to pandoc-pyplot let inclusionKeys = [ targetKey, altTextKey, captionKey ] filteredAttrs = filter (\(k,_) -> k `notElem` inclusionKeys) attrs image = Image (id', cls, filteredAttrs) [Str $ alt spec] (figurePath, "") srcTarget = (sourcePath, "Click on this figure to see the source code") -- TODO: use FigureSpec caption -- We make the figure be a link to the source code return $ Right $ Para [ Link nullAttr [image] srcTarget ] makePlot' x = return $ Right x -- | Translate filter error to an error message 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 BlockingCallError = "Script contains a blocking call to show, like 'plt.show()'" -- | Highest-level function that can be walked over a Pandoc tree. -- All code blocks that have the 'plot_target' parameter will be considered -- figures. makePlot :: Block -> IO Block makePlot = makePlot' >=> either (fail . showError) return