packages feed

pandoc-pyplot 1.1.0.0 → 2.0.0.0

raw patch · 10 files changed

+685/−472 lines, 10 filesdep +hashabledep +randomdep +textPVP ok

version bump matches the API change (PVP)

Dependencies added: hashable, random, text

API changes (from Hackage documentation)

- Text.Pandoc.Filter.Pyplot: MissingDirectoryError :: FilePath -> PandocPyplotError
- Text.Pandoc.Filter.Scripting: addPlotCapture :: FilePath -> PythonScript -> PythonScript
+ Text.Pandoc.Filter.FigureSpec: EPS :: SaveFormat
+ Text.Pandoc.Filter.FigureSpec: FigureSpec :: String -> PythonScript -> SaveFormat -> FilePath -> Int -> Attr -> FigureSpec
+ Text.Pandoc.Filter.FigureSpec: JPG :: SaveFormat
+ Text.Pandoc.Filter.FigureSpec: PDF :: SaveFormat
+ Text.Pandoc.Filter.FigureSpec: PNG :: SaveFormat
+ Text.Pandoc.Filter.FigureSpec: SVG :: SaveFormat
+ Text.Pandoc.Filter.FigureSpec: [blockAttrs] :: FigureSpec -> Attr
+ Text.Pandoc.Filter.FigureSpec: [caption] :: FigureSpec -> String
+ Text.Pandoc.Filter.FigureSpec: [directory] :: FigureSpec -> FilePath
+ Text.Pandoc.Filter.FigureSpec: [dpi] :: FigureSpec -> Int
+ Text.Pandoc.Filter.FigureSpec: [saveFormat] :: FigureSpec -> SaveFormat
+ Text.Pandoc.Filter.FigureSpec: [script] :: FigureSpec -> PythonScript
+ Text.Pandoc.Filter.FigureSpec: addPlotCapture :: FigureSpec -> PythonScript
+ Text.Pandoc.Filter.FigureSpec: data FigureSpec
+ Text.Pandoc.Filter.FigureSpec: data SaveFormat
+ Text.Pandoc.Filter.FigureSpec: extension :: SaveFormat -> String
+ Text.Pandoc.Filter.FigureSpec: figurePath :: FigureSpec -> FilePath
+ Text.Pandoc.Filter.FigureSpec: hiresFigurePath :: FigureSpec -> FilePath
+ Text.Pandoc.Filter.FigureSpec: instance Data.Hashable.Class.Hashable Text.Pandoc.Filter.FigureSpec.FigureSpec
+ Text.Pandoc.Filter.FigureSpec: saveFormatFromString :: String -> Maybe SaveFormat
+ Text.Pandoc.Filter.Pyplot: captionKey :: String
+ Text.Pandoc.Filter.Pyplot: directoryKey :: String
+ Text.Pandoc.Filter.Pyplot: dpiKey :: String
+ Text.Pandoc.Filter.Pyplot: includePathKey :: String
+ Text.Pandoc.Filter.Pyplot: saveFormatKey :: String
- Text.Pandoc.Filter.Scripting: type PythonScript = String
+ Text.Pandoc.Filter.Scripting: type PythonScript = Text

Files

CHANGELOG.md view
@@ -2,6 +2,20 @@  pandoc-pyplot uses [Semantic Versioning](http://semver.org/spec/v2.0.0.html) +Release 2.0.0.0+---------------++Many **breaking changes** in this release:++* `pandoc-pyplot` will now determine the filename based on hashing the figure content. Therefore, figures will only be re-generated if necessary.+* Removed the ability to control the filename and format directly using the `plot_target=...` attribute.+* Added the ability to control the directory in which figures will be saved using the `directory=...` attribute.+* Added the possibility to control the figures dots-per-inch (i.e. pixel density) with the `dpi=...` attribute.+* Added the ability to control the figure format with the `format=...` attribute. Possible values are currently `"png"`, `"svg"`, `"pdf"`, and `"jpg"`/`"jpeg"`.+* The confusing `plot_alt=...` attribute has been renamed to `caption=...` for obvious reasons.+* The `plot_include=...` attribute has been renamed to `include=...`.+* Added the generation of a higher resolution figure for every figure `pandoc-pyplot` understands.+ Release 1.1.0.0 --------------- 
LICENSE.md view
@@ -1,23 +1,23 @@-[The MIT License (MIT)][]--Copyright (c) 2018 Author name here--Permission is hereby granted, free of charge, to any person obtaining a copy of-this software and associated documentation files (the "Software"), to deal in-the Software without restriction, including without limitation the rights to-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies-of the Software, and to permit persons to whom the Software is furnished to do-so, subject to the following conditions:--The above copyright notice and this permission notice shall be included in all-copies or substantial portions of the Software.--THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE-SOFTWARE.--[The MIT License (MIT)]: https://opensource.org/licenses/MIT+[The MIT License (MIT)][]
+
+Copyright (c) 2019 Laurent P. René de Cotret
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+[The MIT License (MIT)]: https://opensource.org/licenses/MIT
README.md view
@@ -8,14 +8,14 @@ 
 ## Usage
 
-The filter recognizes code blocks with the `plot_target` attribute present. It will run the script in the associated code block in a Python interpreter and capture the generated Matplotlib figure. This captured figure will be saved in the located specific by `plot_target`.
+The filter recognizes code blocks with the `pyplot` class present. It will run the script in the associated code block in a Python interpreter and capture the generated Matplotlib figure.
 
 ### Basic example
 
 Here is a basic example using the scripting `matplotlib.pyplot` API:
 
 ```markdown
-    ```{plot_target=my_figure.jpg}
+    ```{.pyplot}
     import matplotlib.pyplot as plt
 
     plt.figure()
@@ -24,10 +24,6 @@     ```
 ```
 
-`pandoc-pyplot` will determine whether the `plot_target` is a relative or absolute path. In case of a relative path (like above), all paths will be considered relative to the current working directory.
-
-We can control the format of the output file by changing the `plot_target` file extension. All formats supported by Matplotlib on your machine are available.
-
 Putting the above in `input.md`, we can then generate the plot and embed it:
 
 ```bash
@@ -40,18 +36,20 @@ pandoc --filter pandoc-pyplot input.md --output output.pdf
 ```
 
-or any other output format you want. There are more examples in the source repository, in the `\examples` directory.
+or any other output format you want. `pandoc-pyplot` is efficient, too: it will detect which figures should be re-generated, and skip the others.
 
+There are more examples in the [source repository](https://github.com/LaurentRDC/pandoc-pyplot), in the `\examples` directory.
+
 ### Link to source code
 
 In case of an output format that supports links (e.g. HTML), the embedded image generated by `pandoc-pyplot` will be a link to the source code which was used to generate the file. Therefore, other people can see what Python code was used to create your figures.
 
 ### Captions
 
-You can also specify a caption for your image. This is done using the optional `plot_alt` parameter:
+You can also specify a caption for your image. This is done using the optional `caption` parameter:
 
 ```markdown
-    ```{plot_target=my_figure.jpg plot_alt="This is a simple figure"}
+    ```{.pyplot caption="This is a simple figure"}
     import matplotlib.pyplot as plt
 
     plt.figure()
@@ -62,7 +60,7 @@ 
 ### Including scripts
 
-If you find yourself always repeating some steps, inclusion of scripts is possible using the `plot_include` parameter. For example, if you want all plots to have the [`ggplot`](https://matplotlib.org/tutorials/introductory/customizing.html#sphx-glr-tutorials-introductory-customizing-py) style, you can write a very short preamble `style.py` like so:
+If you find yourself always repeating some steps, inclusion of scripts is possible using the `include` parameter. For example, if you want all plots to have the [`ggplot`](https://matplotlib.org/tutorials/introductory/customizing.html#sphx-glr-tutorials-introductory-customizing-py) style, you can write a very short preamble `style.py` like so:
 
 ```python
 import matplotlib.pyplot as plt
@@ -72,7 +70,7 @@ and include it in your document as follows:
 
 ```markdown
-    ```{plot_target=my_figure.jpg plot_include=style.py}
+    ```{.pyplot include=style.py}
     plt.figure()
     plt.plot([0,1,2,3,4], [1,2,3,4,5])
     plt.title('This is an example figure')
@@ -82,7 +80,7 @@ Which is equivalent to writing the following markdown:
 
 ```markdown
-    ```{plot_target=my_figure.jpg}
+    ```{.pyplot}
     import matplotlib.pyplot as plt
     plt.style.use('ggplot')
 
@@ -92,7 +90,7 @@     ```
 ```
 
-This `plot_include` parameter is perfect for longer documents with many plots. Simply define the style you want in a separate script! You can also import packages this way, or define functions you often use.
+This `include` parameter is perfect for longer documents with many plots. Simply define the style you want in a separate script! You can also import packages this way, or define functions you often use.
 
 ## Installation
 
@@ -100,6 +98,10 @@ 
 Windows binaries are available on [GitHub](https://github.com/LaurentRDC/pandoc-pyplot/releases). Place the executable in a location that is in your PATH to be able to call it.
 
+### Installers
+
+Starting with `pandoc-pyplot` version 1.1.0.0, Windows installers are also made available thanks to [Inno Setup](http://www.jrsoftware.org/isinfo.php). 
+
 ### From Hackage/Stackage
 
 `pandoc-pyplot` is available on Hackage. Using the [`cabal-install`](https://www.haskell.org/cabal/) tool:
@@ -121,7 +123,7 @@ Building from source can be done using [`stack`](https://docs.haskellstack.org/en/stable/README/) or [`cabal`](https://www.haskell.org/cabal/):
 
 ```bash
-git clone github.com/LaurentRDC/pandoc-pyplot.git
+git clone https://github.com/LaurentRDC/pandoc-pyplot
 cd pandoc-pylot
 stack install # Alternatively, `cabal install`
 ```
@@ -130,9 +132,9 @@ 
 ### Requirements
 
-This filter only works with the Matplotlib plotting library. Therefore, you a Python interpreter and at least [Matplotlib](https://matplotlib.org/) installed. The python interpreter is expected to be discoverable using the name `"python"` (as opposed to `"python3"`, for example)
+This filter only works with the Matplotlib plotting library. Therefore, you a Python interpreter and at least [Matplotlib](https://matplotlib.org/) installed. The Python interpreter is expected to be discoverable using the name `"python"` (as opposed to `"python3"`, for example)
 
-The filter program must be in your `PATH`. In case it is, you can use the filter with Pandoc as follows:
+You can use the filter with Pandoc as follows:
 
 ```bash
 pandoc --filter pandoc-pyplot input.md --output output.html
@@ -177,4 +179,4 @@ 
 ## Warning
 
-Do not run this filter on unknown documents. There is nothing in `pandoc-pyplot` that can stop a Python script from performing evil actions. This is the reason this package is deemed __unsafe__ in the parlance of [Safe Haskell](https://ghc.haskell.org/trac/ghc/wiki/SafeHaskell).
+Do not run this filter on unknown documents. There is nothing in `pandoc-pyplot` that can stop a Python script from performing **evil actions**. This is the reason this package is deemed __unsafe__ in the parlance of [Safe Haskell](https://ghc.haskell.org/trac/ghc/wiki/SafeHaskell).
executable/Main.hs view
@@ -1,41 +1,42 @@-{-# LANGUAGE LambdaCase #-}-module Main where--import System.Environment        (getArgs)--import Text.Pandoc.JSON          (toJSONFilter)-import Text.Pandoc.Filter.Pyplot (makePlot)--import qualified Data.Version as V-import Paths_pandoc_pyplot       (version)---- The formatting is borrowed from Python's argparse library-help :: String-help = "\n\-    \\n\-    \   usage: pandoc-pyplot [-h, --help] [-v, --version] \n\-    \\n\-    \   This pandoc filter generates plots from Python code blocks using Matplotlib. \n\-    \   This allows to keep documentation and figures up-to-date.\n\-    \\n\-    \   Optional arguments:\n\-    \       -h, --help     Show this help message and exit\n\-    \       -v, --version  Show version number and exit \n\-    \\n\-    \   To use with pandoc: \n\-    \       pandoc --filter pandoc-pyplot input.md --output output.html\n\-    \\n\-    \   More information can be found in the repository README, located at \n\-    \       https://github.com/LaurentRDC/pandoc-pyplot\n"--main :: IO ()-main = do-    getArgs >>=-        \case-            (arg:_) -                | arg `elem` ["-h", "--help"]    -> showHelp-                | arg `elem` ["-v", "--version"] -> showVersion-            _ -> toJSONFilter makePlot -    where-        showHelp    = putStrLn help-        showVersion = putStrLn (V.showVersion version)+{-# LANGUAGE LambdaCase #-}
+
+module Main where
+
+import           System.Environment        (getArgs)
+
+import           Text.Pandoc.Filter.Pyplot (makePlot)
+import           Text.Pandoc.JSON          (toJSONFilter)
+
+import qualified Data.Version              as V
+import           Paths_pandoc_pyplot       (version)
+
+-- The formatting is borrowed from Python's argparse library
+help :: String
+help =
+    "\n\
+    \\n\
+    \   usage: pandoc-pyplot [-h, --help] [-v, --version] \n\
+    \\n\
+    \   This pandoc filter generates plots from Python code blocks using Matplotlib. \n\
+    \   This allows to keep documentation and figures in perfect synchronicity.\n\
+    \\n\
+    \   Optional arguments:\n\
+    \       -h, --help     Show this help message and exit\n\
+    \       -v, --version  Show version number and exit \n\
+    \\n\
+    \   To use with pandoc: \n\
+    \       pandoc --filter pandoc-pyplot input.md --output output.html\n\
+    \\n\
+    \   More information can be found in the repository README, located at \n\
+    \       https://github.com/LaurentRDC/pandoc-pyplot\n"
+
+main :: IO ()
+main = do
+    getArgs >>= \case
+        (arg:_)
+            | arg `elem` ["-h", "--help"] -> showHelp
+            | arg `elem` ["-v", "--version"] -> showVersion
+        _ -> toJSONFilter makePlot
+  where
+    showHelp = putStrLn help
+    showVersion = putStrLn (V.showVersion version)
pandoc-pyplot.cabal view
@@ -1,73 +1,78 @@-name:           pandoc-pyplot-version:        1.1.0.0-cabal-version:  >= 1.12-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-homepage:       https://github.com/LaurentRDC/pandoc-pyplot#readme-bug-reports:    https://github.com/LaurentRDC/pandoc-pyplot/issues-author:         Laurent P. René de Cotret-maintainer:     Laurent P. René de Cotret-license:        MIT-license-file:   LICENSE.md-build-type:     Simple-extra-source-files:-    CHANGELOG.md-    LICENSE.md-    README.md-    stack.yaml-    test/fixtures/*.py--source-repository head-    type: git-    location: https://github.com/LaurentRDC/pandoc-pyplot--library-    exposed-modules:-        Text.Pandoc.Filter.Pyplot-        Text.Pandoc.Filter.Scripting-    other-modules:-        Paths_pandoc_pyplot-    hs-source-dirs:-        src-    ghc-options: -Wall -Wcompat-    build-depends:-          base >=4 && <5-        , containers-        , directory-        , filepath-        , pandoc-types >1.12 && <2-        , temporary-        , typed-process-    default-language: Haskell2010--executable pandoc-pyplot-    main-is: Main.hs-    other-modules:-        Paths_pandoc_pyplot-    hs-source-dirs:-        executable-    ghc-options: -Wall -Wcompat -rtsopts -threaded -with-rtsopts=-N-    build-depends:-          base >=4 && <5-        , pandoc-pyplot-        , pandoc-types >1.12 && <2-    default-language: Haskell2010--test-suite tests-    type: exitcode-stdio-1.0-    hs-source-dirs:  test-    main-is:         Main.hs-    build-depends:   base                 >= 4 && < 5-                   , directory-                   , filepath-                   , pandoc-types         >= 1.12 && <= 2-                   , pandoc-pyplot-                   , tasty-                   , tasty-hunit-                   , tasty-hspec-                   , temporary-                   , hspec-                   , hspec-expectations-    default-language: Haskell2010-  +name:           pandoc-pyplot
+version:        2.0.0.0
+cabal-version:  >= 1.12
+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
+homepage:       https://github.com/LaurentRDC/pandoc-pyplot#readme
+bug-reports:    https://github.com/LaurentRDC/pandoc-pyplot/issues
+author:         Laurent P. René de Cotret
+maintainer:     Laurent P. René de Cotret
+license:        MIT
+license-file:   LICENSE.md
+build-type:     Simple
+extra-source-files:
+    CHANGELOG.md
+    LICENSE.md
+    README.md
+    stack.yaml
+    test/fixtures/*.py
+
+source-repository head
+    type: git
+    location: https://github.com/LaurentRDC/pandoc-pyplot
+
+library
+    exposed-modules:
+        Text.Pandoc.Filter.Pyplot
+        Text.Pandoc.Filter.Scripting
+        Text.Pandoc.Filter.FigureSpec
+    other-modules:
+        Paths_pandoc_pyplot
+    hs-source-dirs:
+        src
+    ghc-options: -Wall -Wcompat
+    build-depends:
+          base >=4 && <5
+        , containers
+        , directory
+        , filepath
+        , hashable > 1 && < 2
+        , pandoc-types >1.12 && <2
+        , temporary
+        , text
+        , typed-process
+        , random > 1 && < 2
+    default-language: Haskell2010
+
+executable pandoc-pyplot
+    main-is: Main.hs
+    other-modules:
+        Paths_pandoc_pyplot
+    hs-source-dirs:
+        executable
+    ghc-options: -Wall -Wcompat -rtsopts -threaded -with-rtsopts=-N
+    build-depends:
+          base >=4 && <5
+        , pandoc-pyplot
+        , pandoc-types >1.12 && <2
+    default-language: Haskell2010
+
+test-suite tests
+    type: exitcode-stdio-1.0
+    hs-source-dirs:  test
+    main-is:         Main.hs
+    build-depends:   base                 >= 4 && < 5
+                   , directory
+                   , filepath
+                   , hspec
+                   , hspec-expectations
+                   , pandoc-types         >= 1.12 && <= 2
+                   , pandoc-pyplot
+                   , tasty
+                   , tasty-hunit
+                   , tasty-hspec
+                   , temporary
+                   , text
+    default-language: Haskell2010
+  
+ src/Text/Pandoc/Filter/FigureSpec.hs view
@@ -0,0 +1,107 @@+{-# LANGUAGE OverloadedStrings #-}
+
+{-|
+Module      : Text.Pandoc.Filter.FigureSpec
+Copyright   : (c) Laurent P René de Cotret, 2019
+License     : MIT
+Maintainer  : laurent.decotret@outlook.com
+Stability   : internal
+Portability : portable
+
+This module defines types and functions that help
+with keeping track of figure specifications
+-}
+module Text.Pandoc.Filter.FigureSpec
+    ( FigureSpec(..)
+    , SaveFormat(..)
+    , saveFormatFromString
+    , figurePath
+    , hiresFigurePath
+    , addPlotCapture
+    -- for testing purposes
+    , extension
+    ) where
+
+import           Data.Hashable                (Hashable, hash, hashWithSalt)
+import qualified Data.Text                    as T
+
+import           System.FilePath              (FilePath, addExtension,
+                                               replaceExtension, (</>))
+
+import           Text.Pandoc.Definition       (Attr)
+import           Text.Pandoc.Filter.Scripting (PythonScript)
+
+data SaveFormat
+    = PNG
+    | PDF
+    | SVG
+    | JPG
+    | EPS
+
+-- | Parse an image save format string
+saveFormatFromString :: String -> Maybe SaveFormat
+saveFormatFromString s
+    | s `elem` ["png", "PNG", ".png"] = Just PNG
+    | s `elem` ["pdf", "PDF", ".pdf"] = Just PDF
+    | s `elem` ["svg", "SVG", ".svg"] = Just SVG
+    | s `elem` ["jpg", "jpeg", "JPG", "JPEG", ".jpg", ".jpeg"] = Just JPG
+    | s `elem` ["eps", "EPS", ".eps"] = Just EPS
+    | otherwise = Nothing
+
+-- | Save format file extension
+extension :: SaveFormat -> String
+extension PNG = ".png"
+extension PDF = ".pdf"
+extension SVG = ".svg"
+extension JPG = ".jpg"
+extension EPS = ".eps"
+
+-- | Datatype containing all parameters required
+-- to run pandoc-pyplot
+data FigureSpec = FigureSpec
+    { caption    :: String -- ^ Figure caption.
+    , script     :: PythonScript -- ^ Source code for the figure.
+    , saveFormat :: SaveFormat -- ^ Save format of the figure
+    , directory  :: FilePath -- ^ Directory where to save the file
+    , dpi        :: Int -- ^ Dots-per-inch of figure
+    , blockAttrs :: Attr -- ^ Attributes not related to @pandoc-pyplot@ will be propagated.
+    }
+
+instance Hashable FigureSpec where
+    hashWithSalt salt spec =
+        hashWithSalt salt (caption spec, script spec, directory spec, dpi spec, blockAttrs spec)
+
+-- | Determine the path a figure should have.
+figurePath :: FigureSpec -> FilePath
+figurePath spec = (directory spec </> stem spec)
+  where
+    stem = flip addExtension ext . show . hash
+    ext = extension . saveFormat $ spec
+
+-- | The path to the high-resolution figure.
+hiresFigurePath :: FigureSpec -> FilePath
+hiresFigurePath spec = flip replaceExtension (".hires" <> ext) . figurePath $ spec
+  where
+    ext = extension . saveFormat $ spec
+
+-- | Modify a Python plotting script to save the figure to a filename.
+-- An additional file (with extension PNG) will also be captured.
+addPlotCapture ::
+       FigureSpec -- ^ Path where to save the figure
+    -> PythonScript -- ^ Code block with added capture
+addPlotCapture spec =
+    mconcat
+        [ script spec
+        , "\nimport matplotlib.pyplot as plt" -- Just in case
+        , plotCapture (figurePath spec) (dpi spec)
+        , plotCapture (hiresFigurePath spec) (minimum [200, 2 * dpi spec])
+        ]
+  where
+    plotCapture fname' dpi' =
+        mconcat
+            [ "\nplt.savefig("
+            , T.pack $ show fname' -- show is required for quotes
+            , ", dpi="
+            , T.pack $ show dpi'
+            , ")"
+            ]
src/Text/Pandoc/Filter/Pyplot.hs view
@@ -1,35 +1,43 @@-{-# LANGUAGE MultiWayIf #-}
-{-# LANGUAGE Unsafe     #-}
+{-# LANGUAGE MultiWayIf        #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# 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
+Copyright   : (c) Laurent P René de Cotret, 2019
 License     : MIT
 Maintainer  : laurent.decotret@outlook.com
 Stability   : stable
 Portability : portable
 
-This module defines a Pandoc filter @makePlot@ that can be 
+This module defines a Pandoc filter @makePlot@ that can be
 used to walk over a Pandoc document and generate figures from
 Python code blocks.
 
-The syntax for code blocks is simple, Code blocks with the @plot_target=...@
+The syntax for code blocks is simple, Code blocks with the @.pyplot@
 attribute will trigger the filter. The code block will be reworked into a Python
-script and the output figure will be captured.
+script and the output figure will be captured, along with a high-resolution version
+of the figure and the source code used to generate the figure.
 
+To trigger pandoc-pyplot, the following is __required__:
+
+    * @.pyplot@: Trigger pandoc-pyplot but let it decide on a filename
+
 Here are the possible attributes what pandoc-pyplot understands:
 
-    * @plot_target=...@ (_required_): Filepath where the resulting figure should be saved.
-    * @plot_alt="..."@ (_optional_): Specify a plot caption (or alternate text).
-    * @plot_include=...@ (_optional_): Path to a Python script to include before the code block. 
-    Ideal to avoid repetition over many figures.
+    * @target=...@: Filepath where the resulting figure should be saved.
+    * @directory=...@ : Directory where to save the figure.
+    * @caption="..."@: Specify a plot caption (or alternate text).
+    * @dpi=...@: Specify a value for figure resolution, or dots-per-inch. Default is 80DPI.
+    * @include=...@: Path to a Python script to include before the code block. Ideal to avoid repetition over many figures.
 
 Here are some example blocks in Markdown:
 
 @
 This is a paragraph
 
-```{plot_target=my_figure.jpg plot_alt="This is a caption."}
+```{.pyplot caption="This is a caption."}
 import matplotlib.pyplot as plt
 
 plt.figure()
@@ -37,139 +45,192 @@ plt.title('This is an example figure')
 ```
 @
+
+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:
+
+@
+import Text.Pandoc.Filter.Pyplot (plotTransform)
+
+import Hakyll
+
+-- Unsafe compiler is required because of the interaction
+-- in IO (i.e. running an external Python script).
+makePlotPandocCompiler :: Compiler (Item String)
+makePlotPandocCompiler =
+  pandocCompilerWithTransformM
+    defaultHakyllReaderOptions
+    defaultHakyllWriterOptions
+    (unsafeCompiler . plotTransform)
+@
+
 -}
-module Text.Pandoc.Filter.Pyplot (
-        makePlot
-      , makePlot' -- For testing
-      , plotTransform
-      , PandocPyplotError(..)
+module Text.Pandoc.Filter.Pyplot
+    ( makePlot
+    , plotTransform
+    , PandocPyplotError(..)
+      -- For testing purposes only
+    , makePlot'
+    , directoryKey
+    , captionKey
+    , dpiKey
+    , includePathKey
+    , saveFormatKey
     ) where
 
-import           Control.Monad                  ((>=>))
-import qualified Data.Map.Strict                as M
-import           Data.Maybe                     (fromMaybe)
-import           Data.Monoid                    ((<>))
-import           System.Directory               (doesDirectoryExist)
-import           System.FilePath                (isValid, replaceExtension, takeDirectory)
+import           Control.Monad                 ((>=>))
 
+import           Data.List                     (intersperse)
+
+import qualified Data.Map.Strict               as Map
+import           Data.Maybe                    (fromMaybe)
+import           Data.Monoid                   ((<>))
+import qualified Data.Text                     as T
+import qualified Data.Text.IO                  as T
+import           Data.Version                  (showVersion)
+
+import           Paths_pandoc_pyplot           (version)
+
+import           System.Directory              (createDirectoryIfMissing,
+                                                doesFileExist)
+import           System.FilePath               (isValid, makeValid,
+                                                replaceExtension, takeDirectory)
+
 import           Text.Pandoc.Definition
-import           Text.Pandoc.Walk               (walkM)
+import           Text.Pandoc.Walk              (walkM)
 
-import           Text.Pandoc.Filter.Scripting   
+import           Text.Pandoc.Filter.FigureSpec (FigureSpec (..),
+                                                SaveFormat (..), addPlotCapture,
+                                                figurePath, hiresFigurePath,
+                                                saveFormatFromString)
+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()'
-        deriving Eq
+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()'
+    deriving (Eq)
 
+-- | Translate filter error to an error message
 instance Show PandocPyplotError where
-    -- | Translate filter error to an error message
-    show (ScriptError exitcode)          = "Script error: plot could not be generated. Exit code " <> (show exitcode)
-    show (InvalidTargetError fname)      = "Target filename " <> fname <> " is not valid."
-    show (MissingDirectoryError dirname) = "Target directory " <> dirname <> " does not exist." 
-    show BlockingCallError               = "Script contains a blocking call to show, like 'plt.show()'"
+    show (ScriptError exitcode) =
+        "Script error: plot could not be generated. Exit code " <> (show exitcode)
+    show (InvalidTargetError fname) = "Target filename " <> fname <> " is not valid."
+    show BlockingCallError = "Script contains a blocking call to show, like 'plt.show()'"
 
+-- | Keys that pandoc-pyplot will look for in code blocks. These are only exported for testing purposes.
+directoryKey, captionKey, dpiKey, includePathKey, saveFormatKey :: String
+directoryKey = "directory"
 
--- | 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).
-    , script      :: PythonScript   -- ^ Source code for the figure.
-    , includePath :: Maybe FilePath -- ^ Path to a Python to be included before the script.
-    , blockAttrs  :: Attr           -- ^ Attributes not related to @pandoc-pyplot@ will be propagated.
-    }
+captionKey = "caption"
 
--- | Use figure specification to render a full plot script, including everything except plot capture
-renderScript :: FigureSpec -> IO PythonScript
-renderScript spec = do
-    includeScript <- fromMaybe (return "") $ readFile <$> (includePath spec)
-    return $ mconcat [ "# Source code for ", target spec, "\n"
-                     , "# Generated by pandoc-pyplot\n"
-                     , includeScript, "\n", script spec]
+dpiKey = "dpi"
 
--- Keys that pandoc-pyplot will look for in code blocks
-targetKey, altTextKey, includePathKey :: String
-targetKey      = "plot_target"
-altTextKey     = "plot_alt"
-includePathKey = "plot_include"
+includePathKey = "include"
 
+saveFormatKey = "format"
+
+-- | list of all keys related to pandoc-pyplot.
+inclusionKeys :: [String]
+inclusionKeys = [directoryKey, captionKey, dpiKey, includePathKey, saveFormatKey]
+
 -- | Determine inclusion specifications from Block attributes.
 -- Note that the target key is required, but all other parameters are optional
-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
-            , includePath = M.lookup includePathKey attrs'
-            -- Propagate attributes that are not related to pandoc-pyplot
-            , blockAttrs = (id', cls, filteredAttrs)
-            }
-parseFigureSpec _ = Nothing
+parseFigureSpec :: Block -> IO (Maybe FigureSpec)
+parseFigureSpec (CodeBlock (id', cls, attrs) content)
+    | "pyplot" `elem` cls = Just <$> figureSpec
+    | otherwise = return Nothing
+  where
+    attrs' = Map.fromList attrs
+    filteredAttrs = filter (\(k, _) -> k `notElem` inclusionKeys) attrs
+    dir = makeValid $ Map.findWithDefault "generated" directoryKey attrs'
+    format = fromMaybe (PNG) $ saveFormatFromString $ Map.findWithDefault "png" saveFormatKey attrs'
+    includePath = Map.lookup includePathKey attrs'
+    figureSpec :: IO FigureSpec
+    figureSpec = do
+        includeScript <- fromMaybe (return "") $ T.readFile <$> includePath
+        let header = "# Generated by pandoc-pyplot " <> ((T.pack . showVersion) version)
+            fullScript = mconcat $ intersperse "\n" [header, includeScript, T.pack content]
+            caption' = Map.findWithDefault mempty captionKey attrs'
+            dpi' = read $ Map.findWithDefault "80" dpiKey attrs'
+            blockAttrs' = (id', filter (/= "pyplot") cls, filteredAttrs)
+        return $ FigureSpec caption' fullScript format dir dpi' blockAttrs'
+parseFigureSpec _ = return Nothing
 
+-- | Check figure specifications for common mistakes
+validateSpec :: FigureSpec -> Maybe PandocPyplotError
+validateSpec spec
+    | not (isValid path) = Just $ InvalidTargetError path
+    | hasBlockingShowCall rendered = Just $ BlockingCallError
+    | otherwise = Nothing
+  where
+    path = figurePath spec
+    rendered = script spec
+
+-- | Run the Python script. In case the file already exists, we can safely assume
+-- there is no need to re-run it.
+runScriptIfNecessary :: FigureSpec -> IO ScriptResult
+runScriptIfNecessary spec = do
+    createDirectoryIfMissing True . takeDirectory $ figurePath spec
+    fileAlreadyExists <- doesFileExist $ figurePath spec
+    if fileAlreadyExists
+        then return ScriptSuccess
+        else do
+            result <- runTempPythonScript $ addPlotCapture spec
+            case result of
+                ScriptFailure code -> return $ ScriptFailure code
+                ScriptSuccess
+                    -- 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
+                 -> do
+                    let sourcePath = replaceExtension (figurePath spec) ".txt"
+                    T.writeFile sourcePath $ script spec
+                    return ScriptSuccess
+
 -- | Main routine to include Matplotlib plots.
--- Code blocks containing the attributes @plot_target@ are considered
+-- Code blocks containing the attributes @.pyplot@ 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' block = 
-    case parseFigureSpec block of
-        -- Could not parse - leave code block unchanged
+makePlot' block = do
+    parsed <- parseFigureSpec block
+    case parsed of
         Nothing -> return $ Right block
-        -- Could parse : run the script and capture output
-        Just spec -> do
-            
-            -- Rendered script, including possible inclusions and other additions
-            -- except the plot capture.
-            rendered <- renderScript spec
-
-            let figurePath = target spec
-                figureDir = takeDirectory figurePath
-            
-            -- Check that the directory in which to save the figure exists
-            validDirectory <- doesDirectoryExist $ takeDirectory figurePath
-            
-            if | not (isValid figurePath)         -> return $ Left $ InvalidTargetError figurePath
-               | not validDirectory               -> return $ Left $ MissingDirectoryError figureDir
-               | hasBlockingShowCall rendered     -> return $ Left $ BlockingCallError
-               | otherwise -> do 
-                    
-                -- Running the script
-                -- A plot capture (plt.savefig(...)) is added as well
-                result <- runTempPythonScript $ addPlotCapture (target spec) rendered
-                
-                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 rendered
-                        
-                        -- Propagate attributes that are not related to pandoc-pyplot
-                        let relevantAttrs = blockAttrs spec
-                            srcTarget = Link nullAttr [Str "Source code"] (sourcePath, "")
-                            caption'   = [Str $ alt spec, Space, Str "(", srcTarget, Str ")"]
+        Just spec ->
+            case validateSpec spec of
+                Just err -> return $ Left err
+                Nothing -> do
+                    result <- runScriptIfNecessary spec
+                    case result of
+                        ScriptFailure code -> return $ Left $ ScriptError code
+                        ScriptSuccess -> do
+                            let relevantAttrs = blockAttrs spec
+                                sourcePath = replaceExtension (figurePath spec) ".txt"
+                                hiresPath = hiresFigurePath spec
+                                srcTarget = Link nullAttr [Str "Source code"] (sourcePath, "")
+                                hiresTarget = Link nullAttr [Str "high res."] (hiresPath, "")
+                                -- TODO: use pandoc-types Builder module
+                                caption' =
+                                    [ Str $ caption spec
+                                    , Space
+                                    , Str "("
+                                    , srcTarget
+                                    , Str ","
+                                    , Space
+                                    , hiresTarget
+                                    , Str ")"
+                                    ]
                             -- To render images as figures with captions, the target title
                             -- must be "fig:"
                             -- Janky? yes
-                            image     = Image relevantAttrs caption' (figurePath, "fig:")
-
-                        return $ Right $ Para $ [image]
+                                image = Image relevantAttrs caption' (figurePath spec, "fig:")
+                            return $ Right $ Para $ [image]
 
 -- | Highest-level function that can be walked over a Pandoc tree.
--- All code blocks that have the 'plot_target' parameter will be considered
+-- All code blocks that have the '.pyplot' parameter will be considered
 -- figures.
 makePlot :: Block -> IO Block
 makePlot = makePlot' >=> either (fail . show) return
src/Text/Pandoc/Filter/Scripting.hs view
@@ -1,7 +1,9 @@-{-# LANGUAGE Unsafe #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE Unsafe            #-}
+
 {-|
 Module      : Text.Pandoc.Filter.Scripting
-Copyright   : (c) Laurent P René de Cotret, 2018
+Copyright   : (c) Laurent P René de Cotret, 2019
 License     : MIT
 Maintainer  : laurent.decotret@outlook.com
 Stability   : internal
@@ -10,59 +12,62 @@ This module defines types and functions that help
 with running Python scripts.
 -}
-
-module Text.Pandoc.Filter.Scripting (
-      runTempPythonScript
-    , addPlotCapture
+module Text.Pandoc.Filter.Scripting
+    ( runTempPythonScript
     , hasBlockingShowCall
     , PythonScript
     , ScriptResult(..)
-) where
+    ) where
 
-import System.Exit          (ExitCode(..))
-import System.FilePath      ((</>))
-import System.IO.Temp       (getCanonicalTemporaryDirectory)
-import System.Process.Typed (runProcess, shell)
+import           Data.Text            (Text)
+import qualified Data.Text            as T
+import qualified Data.Text.IO         as T
 
-import Data.Monoid          (Any(..), (<>))
+import           Data.Hashable        (hash)
 
+import           System.Exit          (ExitCode (..))
+import           System.FilePath      ((</>))
+import           System.IO.Temp       (getCanonicalTemporaryDirectory)
+import           System.Process.Typed (runProcess, shell)
+
+import           Data.Monoid          (Any (..), (<>))
+
 -- | String representation of a Python script
-type PythonScript = String
+type PythonScript = Text
 
 -- | Possible result of running a Python script
-data ScriptResult = ScriptSuccess 
-                  | ScriptFailure Int
+data ScriptResult
+    = ScriptSuccess
+    | ScriptFailure Int
 
 -- | Take a python script in string form, write it in a temporary directory,
--- then execute it. 
-runTempPythonScript :: PythonScript    -- ^ Content of the script
-                    -> IO ScriptResult -- ^ Result with exit code.
-runTempPythonScript script = do
-            -- Write script to temporary directory
-            scriptPath <- (</> "pandoc-pyplot.py") <$>  getCanonicalTemporaryDirectory
-            writeFile scriptPath script
-            -- Execute script
-            ec <- runProcess $ shell $ "python " <> (show scriptPath)
-            case ec of
-                ExitSuccess -> return ScriptSuccess
-                ExitFailure code -> return $ ScriptFailure code
-
--- | Modify a Python plotting script to save the figure to a filename.
-addPlotCapture :: FilePath          -- ^ Path where to save the figure
-               -> PythonScript      -- ^ Raw code block
-               -> 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"
-            ]
+-- then execute it.
+runTempPythonScript ::
+       PythonScript -- ^ Content of the script
+    -> IO ScriptResult -- ^ Result with exit code.
+runTempPythonScript script
+    -- Write script to temporary directory
+    -- We involve the script hash as a temporary filename
+    -- so that there is never any collision
+ = do
+    scriptPath <- (</> hashedPath) <$> getCanonicalTemporaryDirectory
+    T.writeFile scriptPath script
+    -- Execute script
+    ec <- runProcess $ shell $ "python " <> (show scriptPath)
+    case ec of
+        ExitSuccess      -> return ScriptSuccess
+        ExitFailure code -> return $ ScriptFailure code
+    where
+        hashedPath = show . hash $ script
 
 -- | Detect the presence of a blocking show call, for example "plt.show()"
 hasBlockingShowCall :: PythonScript -> Bool
-hasBlockingShowCall script = anyOf
+hasBlockingShowCall script =
+    anyOf
         [ "plt.show()" `elem` scriptLines
+        , "pyplot.show()" `elem` scriptLines
         , "matplotlib.pyplot.show()" `elem` scriptLines
         ]
-    where
-        scriptLines = lines script
-        anyOf xs = getAny $ mconcat $ Any <$> xs
+  where
+    scriptLines = T.lines script
+    anyOf xs = getAny $ mconcat $ Any <$> xs
stack.yaml view
@@ -1,64 +1,65 @@-# This file was automatically generated by 'stack init'-#-# Some commonly used options have been documented as comments in this file.-# For advanced use and comprehensive documentation of the format, please see:-# https://docs.haskellstack.org/en/stable/yaml_configuration/--# Resolver to choose a 'specific' stackage snapshot or a compiler version.-# A snapshot resolver dictates the compiler version and the set of packages-# to be used for project dependencies. For example:-#-# resolver: lts-3.5-# resolver: nightly-2015-09-21-# resolver: ghc-7.10.2-#-# The location of a snapshot can be provided as a file or url. Stack assumes-# a snapshot provided as a file might change, whereas a url resource does not.-#-# resolver: ./custom-snapshot.yaml-# resolver: https://example.com/snapshots/2018-01-01.yaml-resolver: lts-12.10--# User packages to be built.-# Various formats can be used as shown in the example below.-#-# packages:-# - some-directory-# - https://example.com/foo/bar/baz-0.0.2.tar.gz-# - location:-#    git: https://github.com/commercialhaskell/stack.git-#    commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a-# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a-#  subdirs:-#  - auto-update-#  - wai-packages:-- .-# Dependency packages to be pulled from upstream that are not in the resolver-# using the same syntax as the packages field.-# (e.g., acme-missiles-0.3)-# extra-deps: []--# Override default flag values for local packages and extra-deps-# flags: {}--# Extra package databases containing global packages-# extra-package-dbs: []--# Control whether we use the GHC we find on the path-# system-ghc: true-#-# Require a specific version of stack, using version ranges-# require-stack-version: -any # Default-# require-stack-version: ">=1.10"-#-# Override the architecture used by stack, especially useful on Windows-# arch: i386-# arch: x86_64-#-# Extra directories used by stack for building-# extra-include-dirs: [/path/to/dir]-# extra-lib-dirs: [/path/to/dir]-#-# Allow a newer minor version of GHC than the snapshot specifies-# compiler-check: newer-minor+# This file was automatically generated by 'stack init'
+#
+# Some commonly used options have been documented as comments in this file.
+# For advanced use and comprehensive documentation of the format, please see:
+# https://docs.haskellstack.org/en/stable/yaml_configuration/
+
+# Resolver to choose a 'specific' stackage snapshot or a compiler version.
+# A snapshot resolver dictates the compiler version and the set of packages
+# to be used for project dependencies. For example:
+#
+# resolver: lts-3.5
+# resolver: nightly-2015-09-21
+# resolver: ghc-7.10.2
+#
+# The location of a snapshot can be provided as a file or url. Stack assumes
+# a snapshot provided as a file might change, whereas a url resource does not.
+#
+# resolver: ./custom-snapshot.yaml
+# resolver: https://example.com/snapshots/2018-01-01.yaml
+resolver: lts-13.14
+
+# User packages to be built.
+# Various formats can be used as shown in the example below.
+#
+# packages:
+# - some-directory
+# - https://example.com/foo/bar/baz-0.0.2.tar.gz
+# - location:
+#    git: https://github.com/commercialhaskell/stack.git
+#    commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
+# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a
+#  subdirs:
+#  - auto-update
+#  - wai
+packages:
+- .
+# Dependency packages to be pulled from upstream that are not in the resolver
+# using the same syntax as the packages field.
+# (e.g., acme-missiles-0.3)
+extra-deps:
+- hindent-5.2.7@sha256:a0a90f7cc160eeba3816e53f310e16942e68d9a09fe092d9835ea6bbbcd2cd91
+
+# Override default flag values for local packages and extra-deps
+# flags: {}
+
+# Extra package databases containing global packages
+# extra-package-dbs: []
+
+# Control whether we use the GHC we find on the path
+# system-ghc: true
+#
+# Require a specific version of stack, using version ranges
+# require-stack-version: -any # Default
+# require-stack-version: ">=1.10"
+#
+# Override the architecture used by stack, especially useful on Windows
+# arch: i386
+# arch: x86_64
+#
+# Extra directories used by stack for building
+# extra-include-dirs: [/path/to/dir]
+# extra-lib-dirs: [/path/to/dir]
+#
+# Allow a newer minor version of GHC than the snapshot specifies
+# compiler-check: newer-minor
test/Main.hs view
@@ -1,126 +1,143 @@+{-# LANGUAGE FlexibleContexts  #-}
+{-# LANGUAGE OverloadedStrings #-}
 
 module Main where
 
-import Control.Monad     (unless)
-import Data.List         (isInfixOf)
+import           Control.Monad                 (unless)
 
-import Test.Tasty
-import Test.Tasty.HUnit
+import           Data.List                     (isInfixOf)
+import           Data.Text                     (unpack)
 
-import qualified Text.Pandoc.Filter.Pyplot as Filter
-import qualified Text.Pandoc.Filter.Scripting as Filter
-import Text.Pandoc.JSON
+import           Test.Tasty
+import           Test.Tasty.HUnit
 
-import System.Directory (doesFileExist)
-import System.FilePath  ((</>))
-import System.IO.Temp   (getCanonicalTemporaryDirectory)
+import qualified Text.Pandoc.Filter.FigureSpec as P
+import qualified Text.Pandoc.Filter.Pyplot     as P
+import qualified Text.Pandoc.Filter.Scripting  as P
+import           Text.Pandoc.JSON
 
+import           System.Directory              (createDirectory,
+                                                createDirectoryIfMissing,
+                                                doesDirectoryExist,
+                                                doesFileExist, listDirectory,
+                                                removeDirectoryRecursive,
+                                                removePathForcibly)
+import           System.FilePath               (isExtensionOf, (</>))
+import           System.IO.Temp                (getCanonicalTemporaryDirectory)
+
 main :: IO ()
-main = defaultMain $ 
-    testGroup "Text.Pandoc.Filter.Pyplot" 
-        [ testFileCreation
-        , testFileInclusion
-        , testBlockingCallError
-        ]
+main =
+    defaultMain $
+    testGroup
+        "Text.Pandoc.Filter.Pyplot"
+        [testFileCreation, testFileInclusion, testSaveFormat, testBlockingCallError]
 
--- | Create a code block with the right attributes to trigger pandoc-pyplot
-mkPlotCodeBlock :: FilePath            -- ^ Plot target
-                -> String              -- ^ Plot alt description
-                -> String              -- ^ Plot caption
-                -> Maybe FilePath      -- ^ Possible inclusion
-                -> Filter.PythonScript -- ^ Script
-                -> Block
-mkPlotCodeBlock target alt caption include script = CodeBlock attrs script
-    where
-        attrs = case include of
-            Nothing -> ( mempty , mempty
-                       , [ ("plot_target", target)
-                         , ("plot_alt", alt)
-                         , ("plot_caption", caption)
-                         ]
-                       )
-            Just includePath -> ( mempty , mempty
-                                , [ ("plot_target", target)
-                                , ("plot_alt", alt)
-                                , ("plot_caption", caption)
-                                , ("plot_include", includePath)
-                                  ]
-                                )
+plotCodeBlock :: P.PythonScript -> Block
+plotCodeBlock script = CodeBlock (mempty, ["pyplot"], mempty) (unpack script)
 
+addCaption :: String -> Block -> Block
+addCaption caption (CodeBlock (id', cls, attrs) script) =
+    CodeBlock (id', cls, attrs ++ [(P.captionKey, caption)]) script
+
+addDirectory :: FilePath -> Block -> Block
+addDirectory dir (CodeBlock (id', cls, attrs) script) =
+    CodeBlock (id', cls, attrs ++ [(P.directoryKey, dir)]) script
+
+addInclusion :: FilePath -> Block -> Block
+addInclusion inclusionPath (CodeBlock (id', cls, attrs) script) =
+    CodeBlock (id', cls, attrs ++ [(P.includePathKey, inclusionPath)]) script
+
+addSaveFormat :: P.SaveFormat -> Block -> Block
+addSaveFormat saveFormat (CodeBlock (id', cls, attrs) script) =
+    CodeBlock (id', cls, attrs ++ [(P.saveFormatKey, P.extension saveFormat)]) script
+
+addDPI :: Int -> Block -> Block
+addDPI dpi (CodeBlock (id', cls, attrs) script) =
+    CodeBlock (id', cls, attrs ++ [(P.dpiKey, show dpi)]) script
+
 -- | Assert that a file exists
-assertFileExists :: HasCallStack 
-                 => FilePath
-                 -> Assertion
+assertFileExists :: HasCallStack => FilePath -> Assertion
 assertFileExists filepath = do
-    fileExists <- doesFileExist filepath 
+    fileExists <- doesFileExist filepath
     unless fileExists (assertFailure msg)
-    where
-        msg = mconcat ["File ", filepath, " does not exist."]
+  where
+    msg = mconcat ["File ", filepath, " does not exist."]
 
--- | Assert that a list first list is contained, 
+-- | Assert that the first list is contained,
 -- wholly and intact, anywhere within the second.
-assertIsInfix :: (Eq a, Show a, HasCallStack)
-              => [a]
-              -> [a]
-              -> Assertion
-assertIsInfix xs ys = 
-    unless (xs `isInfixOf` ys) (assertFailure msg)
-    where
-        msg = mconcat ["Expected ", show xs, " to be an infix of ", show ys]
+assertIsInfix :: (Eq a, Show a, HasCallStack) => [a] -> [a] -> Assertion
+assertIsInfix xs ys = unless (xs `isInfixOf` ys) (assertFailure msg)
+  where
+    msg = mconcat ["Expected ", show xs, " to be an infix of ", show ys]
 
+-- Ensure a directory is empty but exists.
+ensureDirectoryExistsAndEmpty :: FilePath -> IO ()
+ensureDirectoryExistsAndEmpty dir = do
+    exists <- doesDirectoryExist dir
+    if exists
+        then removePathForcibly dir
+        else return ()
+    createDirectory dir
+
 -------------------------------------------------------------------------------
 -- Test that plot files and source files are created when the filter is run
 testFileCreation :: TestTree
-testFileCreation = testCase "writes output and source files" $ do
-    tempDir <- getCanonicalTemporaryDirectory
-    let codeBlock = mkPlotCodeBlock 
-            (tempDir </> "test.png") 
-            mempty 
-            mempty 
-            Nothing
-            "import matplotlib.pyplot as plt\n"    
-    _ <- Filter.makePlot' codeBlock
-    assertFileExists (tempDir </> "test.png")
-    assertFileExists (tempDir </> "test.txt")
+testFileCreation =
+    testCase "writes output files in appropriate directory" $ do
+        tempDir <- (</> "test-file-creation") <$> getCanonicalTemporaryDirectory
+        ensureDirectoryExistsAndEmpty tempDir
+        let codeBlock = (addDirectory tempDir $ plotCodeBlock "import matplotlib.pyplot as plt\n")
+        _ <- P.makePlot' codeBlock
+        filesCreated <- length <$> listDirectory tempDir
+        assertEqual "" filesCreated 3
 
 -------------------------------------------------------------------------------
 -- Test that included files are found within the source
 testFileInclusion :: TestTree
-testFileInclusion = testCase "includes plot inclusions" $ do
-    tempDir <- getCanonicalTemporaryDirectory
-    
-    let codeBlock = mkPlotCodeBlock 
-            (tempDir </> "test.png") 
-            mempty 
-            mempty 
-            (Just "test/fixtures/include.py") 
-            "import matplotlib.pyplot as plt\n"
-    _ <- Filter.makePlot' codeBlock
+testFileInclusion =
+    testCase "includes plot inclusions" $ do
+        tempDir <- (</> "test-file-inclusion") <$> getCanonicalTemporaryDirectory
+        ensureDirectoryExistsAndEmpty tempDir
+        let codeBlock =
+                (addInclusion "test/fixtures/include.py" $
+                 addDirectory tempDir $ plotCodeBlock "import matplotlib.pyplot as plt\n")
+        _ <- P.makePlot' codeBlock
+        inclusion <- readFile "test/fixtures/include.py"
+        sourcePath <- head . filter (isExtensionOf "txt") <$> listDirectory tempDir
+        src <- readFile (tempDir </> sourcePath)
+        assertIsInfix inclusion src
 
-    inclusion <- readFile "test/fixtures/include.py"
-    src <- readFile (tempDir </> "test.txt")
-    assertIsInfix inclusion src
+-------------------------------------------------------------------------------
+-- Test that the files are saved in the appropriate format
+testSaveFormat :: TestTree
+testSaveFormat =
+    testCase "saves in the appropriate format" $ do
+        tempDir <- (</> "test-safe-format") <$> getCanonicalTemporaryDirectory
+        ensureDirectoryExistsAndEmpty tempDir
+        let codeBlock =
+                (addSaveFormat P.JPG $
+                 addDirectory tempDir $
+                 plotCodeBlock
+                     "import matplotlib.pyplot as plt\nplt.figure()\nplt.plot([1,2], [1,2])")
+        _ <- P.makePlot' codeBlock
+        numberjpgFiles <-
+            length <$> filter (isExtensionOf (P.extension P.JPG)) <$>
+            listDirectory tempDir
+        assertEqual "" numberjpgFiles 2
 
 -------------------------------------------------------------------------------
 -- Test that a script containing a blockign call to matplotlib.pyplot.show
 -- returns the appropriate error
 testBlockingCallError :: TestTree
-testBlockingCallError = testCase "raises an exception for blocking calls" $ do
-    tempDir <- getCanonicalTemporaryDirectory
-
-    let codeBlock = mkPlotCodeBlock 
-            (tempDir </> "test.png") 
-            mempty 
-            mempty 
-            Nothing
-            "import matplotlib.pyplot as plt\nplt.show()"
-    
-    result <- Filter.makePlot' codeBlock
-    case result of
-        Right block -> assertFailure "did not catch the expected blocking call"
-        Left  error -> 
-            if error == Filter.BlockingCallError 
-            then pure () 
-            else assertFailure "did not catch the expected blocking call"
-
+testBlockingCallError =
+    testCase "raises an exception for blocking calls" $ do
+        tempDir <- getCanonicalTemporaryDirectory
+        let codeBlock = plotCodeBlock "import matplotlib.pyplot as plt\nplt.show()"
+        result <- P.makePlot' codeBlock
+        case result of
+            Right block -> assertFailure "did not catch the expected blocking call"
+            Left error ->
+                if error == P.BlockingCallError
+                    then pure ()
+                    else assertFailure "did not catch the expected blocking call"
 -------------------------------------------------------------------------------