packages feed

pandoc-pyplot 1.0.0.1 → 1.0.1.0

raw patch · 5 files changed

+81/−76 lines, 5 files

Files

README.md view
@@ -2,7 +2,7 @@ 
 [![Hackage version](https://img.shields.io/hackage/v/pandoc-pyplot.svg)](http://hackage.haskell.org/package/pandoc-pyplot) [![Stackage version](http://stackage.org/package/pandoc-pyplot/badge/nightly)](http://stackage.org/nightly/package/pandoc-pyplot) [![Build status](https://ci.appveyor.com/api/projects/status/qbmq9cyks5jup48e?svg=true)](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