pandoc-pyplot 1.0.0.1 → 1.0.1.0
raw patch · 5 files changed
+81/−76 lines, 5 files
Files
- README.md +5/−24
- package.yaml +1/−1
- pandoc-pyplot.cabal +2/−2
- src/Text/Pandoc/Filter/Pyplot.hs +66/−38
- src/Text/Pandoc/Filter/Scripting.hs +7/−11
README.md view
@@ -2,7 +2,7 @@ [](http://hackage.haskell.org/package/pandoc-pyplot) [](http://stackage.org/nightly/package/pandoc-pyplot) [](https://ci.appveyor.com/project/LaurentRDC/pandoc-pyplot) -_A Pandoc filter for generating figures with Matplotlib from code directly in documents_ +## A Pandoc filter for generating figures with Matplotlib from code directly in documents Inspired by [sphinx](https://sphinxdoc.org)'s `plot_directive`, `pandoc-pyplot` helps turn Python code present in your documents to embedded Matplotlib figures. @@ -88,35 +88,16 @@ ## Usage as a Haskell library -To include the functionality of `pandoc-pyplot` in a Haskell package, you can use the `makePlot` function: - -```haskell --- From pandoc-types -import Text.Pandoc.Walk (walkM) -import Text.Pandoc.Definition (Pandoc) --- From pandoc-pyplot -import Text.Pandoc.Filter.Pyplot (makePlot) - -transformDocument :: Pandoc -> IO Pandoc -transformDocument = walkM makePlot -``` +To include the functionality of `pandoc-pyplot` in a Haskell package, you can use the `makePlot :: Block -> IO Block` function (for single blocks) or `plotTransform :: Pandoc -> IO Pandoc` function (for entire documents). -## Usage with Hakyll +### Usage with Hakyll -This filter was originally designed to be used with [Hakyll](https://jaspervdj.be/hakyll/). In case you want to use the filter with your own Hakyll setup, you must create a transform function first: +This filter was originally designed to be used with [Hakyll](https://jaspervdj.be/hakyll/). In case you want to use the filter with your own Hakyll setup, you can use a transform function that works on entire documents: ```haskell --- From pandoc-types -import Text.Pandoc (Pandoc) -import Text.Pandoc.Walk (walkM) - --- from pandoc-pyplot -import Text.Pandoc.Filter.Pyplot (makePlot) +import Text.Pandoc.Filter.Pyplot (plotTransform) import Hakyll - -plotTransform :: Pandoc -> IO Pandoc -plotTransform = walkM . makePlot -- Unsafe compiler is required because of the interaction -- in IO (i.e. running an external Python script).
package.yaml view
@@ -1,5 +1,5 @@ name: pandoc-pyplot-version: '1.0.0.1'+version: '1.0.1.0' github: "LaurentRDC/pandoc-pyplot" license: MIT author: "Laurent P. René de Cotret"
pandoc-pyplot.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 86659e92666d842eaf1742bd04d9e7ba408b1a4923a90e88fd641dbfe6a7a0d0+-- hash: 02a0b4cc9681a38e27465380086e167be8805a63959e4299baaca728b3e17c09 name: pandoc-pyplot-version: 1.0.0.1+version: 1.0.1.0 synopsis: A Pandoc filter for including figures generated from Matplotlib description: A pandoc filter for including figures generated from Matplotlib. Keep the document and Python code in the same location. Output from Matplotlib is captured and included as a figure. category: Documentation
src/Text/Pandoc/Filter/Pyplot.hs view
@@ -16,31 +16,50 @@ module Text.Pandoc.Filter.Pyplot ( makePlot , makePlot' + , plotTransform , PandocPyplotError(..) , showError ) where -import Control.Monad ((>=>)) -import qualified Data.Map.Strict as M -import System.FilePath (replaceExtension, isValid) +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 +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 + | MissingDirectoryError FilePath -- ^ Directory where to save figure does not exist | 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) + { target :: FilePath -- ^ filepath where generated figure will be saved + , alt :: String -- ^ Alternate text for the figure (optional) + , caption :: String -- ^ Figure caption (optional) + , script :: PythonScript -- ^ Source code for the figure + , blockAttrs :: Attr -- ^ Attributes not related to @pandoc-pyplot@ will be propagated } +-- | Get the source code for a script including provisions to capture +-- the output. +scriptWithCapture :: FigureSpec -> PythonScript +scriptWithCapture spec = addPlotCapture (target spec) (script spec) + +-- | Determine where to save the script source based on plot target +scriptSourcePath :: FigureSpec -> FilePath +scriptSourcePath spec = replaceExtension (target spec) ".txt" + +-- | Get the source code for a figure script in a presentable way +presentableScript :: FigureSpec -> PythonScript +presentableScript spec = mconcat [ "# Source code for ", target spec, "\n", script spec ] + -- Keys that pandoc-pyplot will look for in code blocks targetKey, altTextKey, captionKey :: String targetKey = "plot_target" @@ -49,23 +68,22 @@ -- | 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 +parseFigureSpec :: Block -> Maybe FigureSpec +parseFigureSpec (CodeBlock (id', cls, attrs) content) = + createInclusion <$> M.lookup targetKey attrs' where - defaultAltText = "Figure generated by pandoc-pyplot" - defaultCaption = mempty + attrs' = M.fromList attrs + inclusionKeys = [ targetKey, altTextKey, captionKey ] + filteredAttrs = filter (\(k,_) -> k `notElem` inclusionKeys) attrs createInclusion fname = FigureSpec - { target = fname - , alt = M.findWithDefault defaultAltText altTextKey attrs - , caption = M.findWithDefault defaultCaption captionKey attrs + { target = fname + , alt = M.findWithDefault "Figure generated by pandoc-pyplot" altTextKey attrs' + , caption = M.findWithDefault mempty captionKey attrs' + , script = content + -- Propagate attributes that are not related to pandoc-pyplot + , blockAttrs = (id', cls, filteredAttrs) } - --- | Format the script source based on figure spec. -formatScriptSource :: FigureSpec -> PythonScript -> PythonScript -formatScriptSource spec script = mconcat [ "# Source code for " <> target spec - , "\n" - , script - ] +parseFigureSpec _ = Nothing -- | Main routine to include Matplotlib plots. -- Code blocks containing the attributes @plot_target@ are considered @@ -73,20 +91,27 @@ -- 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 +makePlot' block = + case parseFigureSpec block of -- Could not parse - leave code block unchanged - Nothing -> return $ Right cb + Nothing -> return $ Right block -- Could parse : run the script and capture output Just spec -> do let figurePath = target spec + figureDir = takeDirectory figurePath + scriptSource = script spec + -- Check that the directory in which to save the figure exists + validDirectory <- doesDirectoryExist figureDir + if | not (isValid figurePath) -> return $ Left $ InvalidTargetError figurePath + | not validDirectory -> return $ Left $ MissingDirectoryError figureDir | hasBlockingShowCall scriptSource -> return $ Left $ BlockingCallError | otherwise -> do - script <- addPlotCapture figurePath scriptSource - result <- runTempPythonScript script + -- Running the script happens on the next line + -- Note that the script is slightly modified to be able to capture the output + result <- runTempPythonScript (scriptWithCapture spec) case result of ScriptFailure code -> return $ Left $ ScriptError code @@ -95,31 +120,34 @@ -- 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 + let sourcePath = scriptSourcePath spec + writeFile sourcePath (presentableScript spec) -- 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") + let relevantAttrs = blockAttrs spec + image = Image relevantAttrs [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()'" +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()'" -- | 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+makePlot = makePlot' >=> either (fail . showError) return + +-- | Walk over an entire Pandoc document, changing appropriate code blocks +-- into figures. +plotTransform :: Pandoc -> IO Pandoc +plotTransform = walkM makePlot
src/Text/Pandoc/Filter/Scripting.hs view
@@ -19,9 +19,8 @@ , ScriptResult(..) ) where -import System.Directory (getCurrentDirectory) import System.Exit (ExitCode(..)) -import System.FilePath ((</>), isAbsolute) +import System.FilePath ((</>)) import System.IO.Temp (getCanonicalTemporaryDirectory) import System.Process.Typed (runProcess, shell) @@ -51,15 +50,12 @@ -- | Modify a Python plotting script to save the figure to a filename. addPlotCapture :: FilePath -- ^ Path where to save the figure -> PythonScript -- ^ Raw code block - -> IO PythonScript -- ^ Code block with added capture -addPlotCapture fname content = do - absFname <- if isAbsolute fname - then (return fname) - else (</> fname) <$> getCurrentDirectory - return $ mconcat [ content - , "\nimport matplotlib.pyplot as plt" -- Just in case - , "\nplt.savefig(" <> show absFname <> ")\n\n" - ] + -> PythonScript -- ^ Code block with added capture +addPlotCapture fname content = + mconcat [ content + , "\nimport matplotlib.pyplot as plt" -- Just in case + , "\nplt.savefig(" <> show fname <> ")\n\n" + ] -- | Detect the presence of a blocking show call, for example "plt.show()" hasBlockingShowCall :: PythonScript -> Bool