packages feed

pandoc-pyplot 2.0.1.0 → 2.1.0.0

raw patch · 14 files changed

+675/−383 lines, 14 filesdep +data-default-classdep +yamlPVP ok

version bump matches the API change (PVP)

Dependencies added: data-default-class, yaml

API changes (from Hackage documentation)

- 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: instance Data.Hashable.Class.Hashable Text.Pandoc.Filter.FigureSpec.FigureSpec
- Text.Pandoc.Filter.FigureSpec: saveFormatFromString :: String -> Maybe SaveFormat
- Text.Pandoc.Filter.FigureSpec: sourceCodePath :: FigureSpec -> FilePath
- Text.Pandoc.Filter.FigureSpec: toImage :: FigureSpec -> Block
- 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: ScriptFailure :: Int -> ScriptResult
- Text.Pandoc.Filter.Scripting: ScriptSuccess :: ScriptResult
- Text.Pandoc.Filter.Scripting: data ScriptResult
- Text.Pandoc.Filter.Scripting: hasBlockingShowCall :: PythonScript -> Bool
- Text.Pandoc.Filter.Scripting: runTempPythonScript :: PythonScript -> IO ScriptResult
- Text.Pandoc.Filter.Scripting: type PythonScript = Text
+ Text.Pandoc.Filter.Pyplot: Configuration :: FilePath -> PythonScript -> SaveFormat -> Int -> String -> Configuration
+ Text.Pandoc.Filter.Pyplot: [defaultDPI] :: Configuration -> Int
+ Text.Pandoc.Filter.Pyplot: [defaultDirectory] :: Configuration -> FilePath
+ Text.Pandoc.Filter.Pyplot: [defaultIncludeScript] :: Configuration -> PythonScript
+ Text.Pandoc.Filter.Pyplot: [defaultSaveFormat] :: Configuration -> SaveFormat
+ Text.Pandoc.Filter.Pyplot: [interpreter] :: Configuration -> String
+ Text.Pandoc.Filter.Pyplot: configuration :: FilePath -> IO Configuration
+ Text.Pandoc.Filter.Pyplot: data Configuration
+ Text.Pandoc.Filter.Pyplot: makePlotWithConfig :: Configuration -> Block -> IO Block
+ Text.Pandoc.Filter.Pyplot: plotTransformWithConfig :: Configuration -> Pandoc -> IO Pandoc
+ Text.Pandoc.Filter.Pyplot.Internal: Configuration :: FilePath -> PythonScript -> SaveFormat -> Int -> String -> Configuration
+ Text.Pandoc.Filter.Pyplot.Internal: EPS :: SaveFormat
+ Text.Pandoc.Filter.Pyplot.Internal: FigureSpec :: String -> PythonScript -> SaveFormat -> FilePath -> Int -> Attr -> FigureSpec
+ Text.Pandoc.Filter.Pyplot.Internal: GIF :: SaveFormat
+ Text.Pandoc.Filter.Pyplot.Internal: JPG :: SaveFormat
+ Text.Pandoc.Filter.Pyplot.Internal: PDF :: SaveFormat
+ Text.Pandoc.Filter.Pyplot.Internal: PNG :: SaveFormat
+ Text.Pandoc.Filter.Pyplot.Internal: SVG :: SaveFormat
+ Text.Pandoc.Filter.Pyplot.Internal: ScriptFailure :: Int -> ScriptResult
+ Text.Pandoc.Filter.Pyplot.Internal: ScriptSuccess :: ScriptResult
+ Text.Pandoc.Filter.Pyplot.Internal: TIF :: SaveFormat
+ Text.Pandoc.Filter.Pyplot.Internal: [blockAttrs] :: FigureSpec -> Attr
+ Text.Pandoc.Filter.Pyplot.Internal: [caption] :: FigureSpec -> String
+ Text.Pandoc.Filter.Pyplot.Internal: [defaultDPI] :: Configuration -> Int
+ Text.Pandoc.Filter.Pyplot.Internal: [defaultDirectory] :: Configuration -> FilePath
+ Text.Pandoc.Filter.Pyplot.Internal: [defaultIncludeScript] :: Configuration -> PythonScript
+ Text.Pandoc.Filter.Pyplot.Internal: [defaultSaveFormat] :: Configuration -> SaveFormat
+ Text.Pandoc.Filter.Pyplot.Internal: [directory] :: FigureSpec -> FilePath
+ Text.Pandoc.Filter.Pyplot.Internal: [dpi] :: FigureSpec -> Int
+ Text.Pandoc.Filter.Pyplot.Internal: [interpreter] :: Configuration -> String
+ Text.Pandoc.Filter.Pyplot.Internal: [saveFormat] :: FigureSpec -> SaveFormat
+ Text.Pandoc.Filter.Pyplot.Internal: [script] :: FigureSpec -> PythonScript
+ Text.Pandoc.Filter.Pyplot.Internal: addPlotCapture :: FigureSpec -> PythonScript
+ Text.Pandoc.Filter.Pyplot.Internal: captionKey :: String
+ Text.Pandoc.Filter.Pyplot.Internal: configuration :: FilePath -> IO Configuration
+ Text.Pandoc.Filter.Pyplot.Internal: data Configuration
+ Text.Pandoc.Filter.Pyplot.Internal: data FigureSpec
+ Text.Pandoc.Filter.Pyplot.Internal: data SaveFormat
+ Text.Pandoc.Filter.Pyplot.Internal: data ScriptResult
+ Text.Pandoc.Filter.Pyplot.Internal: directoryKey :: String
+ Text.Pandoc.Filter.Pyplot.Internal: dpiKey :: String
+ Text.Pandoc.Filter.Pyplot.Internal: extension :: SaveFormat -> String
+ Text.Pandoc.Filter.Pyplot.Internal: figurePath :: FigureSpec -> FilePath
+ Text.Pandoc.Filter.Pyplot.Internal: hasBlockingShowCall :: PythonScript -> Bool
+ Text.Pandoc.Filter.Pyplot.Internal: includePathKey :: String
+ Text.Pandoc.Filter.Pyplot.Internal: inclusionKeys :: [String]
+ Text.Pandoc.Filter.Pyplot.Internal: runTempPythonScript :: String -> PythonScript -> IO ScriptResult
+ Text.Pandoc.Filter.Pyplot.Internal: saveFormatFromString :: String -> Maybe SaveFormat
+ Text.Pandoc.Filter.Pyplot.Internal: saveFormatKey :: String
+ Text.Pandoc.Filter.Pyplot.Internal: sourceCodePath :: FigureSpec -> FilePath
+ Text.Pandoc.Filter.Pyplot.Internal: toImage :: FigureSpec -> Block
+ Text.Pandoc.Filter.Pyplot.Internal: type PythonScript = Text
- Text.Pandoc.Filter.Pyplot: makePlot' :: Block -> IO (Either PandocPyplotError Block)
+ Text.Pandoc.Filter.Pyplot: makePlot' :: Configuration -> Block -> IO (Either PandocPyplotError Block)

Files

CHANGELOG.md view
@@ -1,65 +1,75 @@-# Change log--pandoc-pyplot uses [Semantic Versioning](http://semver.org/spec/v2.0.0.html)--Release 2.0.1.0------------------* Support for Markdown formatting in figure captions, including LaTeX math.--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"`, `"jpg"`/`"jpeg"` and `"eps"`.-* 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------------------* Added the ability to include Python files before code using the `plot_include=script.py` attribute.-* Added a test suite.--Release 1.0.3.0------------------* Fixed an issue where `pandoc-pyplot` would not build with base < 4.9 (#1)--Release 1.0.2.0------------------* Added support for captions using the `plot_alt=...` attribute. For example:--  ```markdown-    ```{plot_target=test.png plot_alt="This is a caption"}-    import matplotlib.pyplot as plt-    plt.figure()-    plt.plot([1,2,3,4,5],[1,2,3,4,5])-    ```-  ```--Release 1.0.1.0------------------* Added `plotTransform :: Pandoc -> IO Pandoc` function to transform entire documents. This makes it easier to integrate `pandoc-pyplot` into Hakyll-based sites!--Release 1.0.0.1------------------* Updated README with fixes and warnings-* Added top-level package documentation compatible with Haddock-* Added Unsafe language extension, as this filter will run arbitrary Python scripts.--Release 1.0.0.0------------------Initial release.--See documentation on [Hackage](https://hackage.haskell.org/package/pandoc-pyplot)+# Change log
+
+pandoc-pyplot uses [Semantic Versioning](http://semver.org/spec/v2.0.0.html)
+
+Release 2.1.0.0
+---------------
+
+* Added support for config files ".pandoc-pyplot.yml", which specify different default values. This is mirrored in the new `Configuration` type and new functions, `makePlotWithConfig` and `plotTransformWithConfig`.
+* Added the ability to specify a different Python interpreter to use.
+* Added support for GIF and TIF files.
+* Added the "-f"/"--formats" command to show supported output figure formats.
+* Added support for GHC 8.2
+* Moved internal modules to `Text.Pandoc.Filter.Pyplot.Internal` module.
+
+Release 2.0.1.0
+---------------
+
+* Support for Markdown formatting in figure captions, including LaTeX math.
+
+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"`, `"jpg"`/`"jpeg"` and `"eps"`.
+* 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
+---------------
+
+* Added the ability to include Python files before code using the `plot_include=script.py` attribute.
+* Added a test suite.
+
+Release 1.0.3.0
+---------------
+
+* Fixed an issue where `pandoc-pyplot` would not build with base < 4.9 (#1)
+
+Release 1.0.2.0
+---------------
+
+* Added support for captions using the `plot_alt=...` attribute. For example:
+
+  ```markdown
+    ```{plot_target=test.png plot_alt="This is a caption"}
+    import matplotlib.pyplot as plt
+    plt.figure()
+    plt.plot([1,2,3,4,5],[1,2,3,4,5])
+    ```
+  ```
+
+Release 1.0.1.0
+---------------
+
+* Added `plotTransform :: Pandoc -> IO Pandoc` function to transform entire documents. This makes it easier to integrate `pandoc-pyplot` into Hakyll-based sites!
+
+Release 1.0.0.1
+---------------
+
+* Updated README with fixes and warnings
+* Added top-level package documentation compatible with Haddock
+* Added Unsafe language extension, as this filter will run arbitrary Python scripts.
+
+Release 1.0.0.0
+---------------
+
+Initial release.
+
+See documentation on [Hackage](https://hackage.haskell.org/package/pandoc-pyplot)
LICENSE.md view
@@ -1,13 +1,13 @@-[The MIT License (MIT)][]
+MIT License
 
 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:
+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.
@@ -19,5 +19,3 @@ 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
@@ -1,12 +1,12 @@ # pandoc-pyplot - A Pandoc filter to generate Matplotlib figures directly in documents
 
-[![Hackage version](https://img.shields.io/hackage/v/pandoc-pyplot.svg)](http://hackage.haskell.org/package/pandoc-pyplot) [![Stackage version (LTS)](http://stackage.org/package/pandoc-pyplot/badge/lts)](http://stackage.org/nightly/package/pandoc-pyplot) [![Stackage version (nightly)](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) ![](https://img.shields.io/badge/license-MIT-green.svg)
+[![Hackage version](https://img.shields.io/hackage/v/pandoc-pyplot.svg)](http://hackage.haskell.org/package/pandoc-pyplot) [![Stackage version (LTS)](http://stackage.org/package/pandoc-pyplot/badge/lts)](http://stackage.org/nightly/package/pandoc-pyplot) [![Stackage version (nightly)](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) ![GitHub](https://img.shields.io/github/license/LaurentRDC/pandoc-pyplot.svg)
 
 `pandoc-pyplot` turns Python code present in your documents to embedded Matplotlib figures.
 
 ## Usage
 
-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.
+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.
 
 Here is a basic example using the scripting `matplotlib.pyplot` API:
 
@@ -32,7 +32,7 @@ pandoc --filter pandoc-pyplot input.md --output output.pdf
 ```
 
-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.
+or any other output format you want.
 
 There are more examples in the [source repository](https://github.com/LaurentRDC/pandoc-pyplot), in the `\examples` directory.
 
@@ -96,6 +96,30 @@ 
 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.
 
+### Configurable
+
+*New in version 2.1.0.0*
+
+To avoid repetition, `pandoc-pyplot` can be configured using simple YAML syntax. `pandoc-pyplot` will look for a `pandoc-pyplot.yml` file in the current working directory:
+
+```yaml
+# You can specify any or all of the following parameters
+interpreter: python36
+directory: mydirectory/
+include: mystyle.py
+format: jpeg
+dpi: 150
+```
+
+These values override the default values, which are equivalent to:
+
+```yaml
+interpreter: python
+directory: generated/
+format: png
+dpi: 80
+```
+
 ## Installation
 
 ### Binaries
@@ -136,7 +160,7 @@ 
 ### 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 name of the Python interpreter to use can be specified in a `.pandoc-pyplot.yml` file; by default, `pandoc-pyplot` will use the `"python"` name.
 
 You can use the filter with Pandoc as follows:
 
@@ -152,7 +176,7 @@ 
 Python exceptions will be printed to screen in case of a problem.
 
-`pandoc-pyplot` has a very limited command-line interface. Take a look at the help available using the `-h` or `--help` argument:
+`pandoc-pyplot` has a limited command-line interface. Take a look at the help available using the `-h` or `--help` argument:
 
 ```bash
 pandoc-pyplot --help
@@ -181,6 +205,8 @@     (unsafeCompiler . plotTransform)
 ```
 
+The `plotTransformWithConfig` is also available for a more configurable set-up.
+
 ## 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**.
executable/Main.hs view
@@ -2,20 +2,28 @@ 
 module Main where
 
+import           Data.Default.Class        (def)
+import           Data.List                 (intersperse)
+
 import           System.Environment        (getArgs)
+import           System.Directory          (doesFileExist)
 
-import           Text.Pandoc.Filter.Pyplot (makePlot)
+import           Text.Pandoc.Filter.Pyplot (makePlotWithConfig, configuration)
+import           Text.Pandoc.Filter.Pyplot.Internal (SaveFormat(..))
 import           Text.Pandoc.JSON          (toJSONFilter)
 
 import qualified Data.Version              as V
 import           Paths_pandoc_pyplot       (version)
 
+supportedSaveFormats :: [SaveFormat]
+supportedSaveFormats = enumFromTo minBound maxBound
+
 -- The formatting is borrowed from Python's argparse library
 help :: String
 help =
     "\n\
     \\n\
-    \   usage: pandoc-pyplot [-h, --help] [-v, --version] \n\
+    \   usage: pandoc-pyplot [-h, --help] [-v, --version] [-f, --formats] \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\
@@ -23,6 +31,7 @@     \   Optional arguments:\n\
     \       -h, --help     Show this help message and exit\n\
     \       -v, --version  Show version number and exit \n\
+    \       -f, --formats  Show available output figure formats and exit \n\
     \\n\
     \   To use with pandoc: \n\
     \       pandoc --filter pandoc-pyplot input.md --output output.html\n\
@@ -32,11 +41,18 @@ 
 main :: IO ()
 main = do
+    configExists <- doesFileExist ".pandoc-pyplot.yml"
+    config <- if configExists
+                then configuration ".pandoc-pyplot.yml"
+                else def
+
     getArgs >>= \case
         (arg:_)
             | arg `elem` ["-h", "--help"] -> showHelp
             | arg `elem` ["-v", "--version"] -> showVersion
-        _ -> toJSONFilter makePlot
+            | arg `elem` ["-f", "--formats"] -> showFormats
+        _ -> toJSONFilter (makePlotWithConfig config)
   where
-    showHelp = putStrLn help
+    showHelp    = putStrLn help
     showVersion = putStrLn (V.showVersion version)
+    showFormats = putStrLn . mconcat . intersperse ", " . fmap show $ supportedSaveFormats
pandoc-pyplot.cabal view
@@ -1,5 +1,5 @@ name:           pandoc-pyplot
-version:        2.0.1.0
+version:        2.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.
@@ -25,10 +25,12 @@ library
     exposed-modules:
         Text.Pandoc.Filter.Pyplot
-        Text.Pandoc.Filter.Scripting
-        Text.Pandoc.Filter.FigureSpec
+        Text.Pandoc.Filter.Pyplot.Internal
     other-modules:
         Paths_pandoc_pyplot
+        Text.Pandoc.Filter.Pyplot.Scripting
+        Text.Pandoc.Filter.Pyplot.FigureSpec
+        Text.Pandoc.Filter.Pyplot.Configuration
     hs-source-dirs:
         src
     ghc-options: -Wall -Wcompat
@@ -36,6 +38,7 @@           base     >=4 && <5
         , containers
         , directory
+        , data-default-class >= 0.1.2
         , filepath
         , hashable > 1 && < 2
         , pandoc   > 2 && < 3
@@ -43,6 +46,7 @@         , temporary
         , text
         , typed-process
+        , yaml >= 0.8.16
     default-language: Haskell2010
 
 executable pandoc-pyplot
@@ -54,6 +58,8 @@     ghc-options: -Wall -Wcompat -rtsopts -threaded -with-rtsopts=-N
     build-depends:
           base >=4 && <5
+        , directory
+        , data-default-class >= 0.1.2
         , pandoc-pyplot
         , pandoc-types >1.12 && <2
     default-language: Haskell2010
@@ -64,6 +70,7 @@     main-is:         Main.hs
     build-depends:   base                 >= 4 && < 5
                    , directory
+                   , data-default-class >= 0.1.2
                    , filepath
                    , hspec
                    , hspec-expectations
− src/Text/Pandoc/Filter/FigureSpec.hs
@@ -1,158 +0,0 @@-{-# 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
-    , toImage
-    , sourceCodePath
-    , figurePath
-    , addPlotCapture
-    -- for testing purposes
-    , extension
-    ) where
-
-import           Control.Monad                (join)
-
-import           Data.Hashable                (Hashable, hash, hashWithSalt)
-import           Data.Maybe                   (fromMaybe)
-import qualified Data.Text                    as T
-
-import           System.FilePath              (FilePath, addExtension,
-                                               replaceExtension, (</>))
-
-import           Text.Pandoc.Definition       
-import           Text.Pandoc.Builder          (imageWith, link, para, fromList, toList)
-import           Text.Pandoc.Filter.Scripting (PythonScript)
-
-import           Text.Pandoc.Class            (runPure)
-import           Text.Pandoc.Extensions       (extensionsFromList, Extension(..))
-import           Text.Pandoc.Options          (def, ReaderOptions(..))
-import           Text.Pandoc.Readers          (readMarkdown)
-
-readerOptions :: ReaderOptions
-readerOptions = def 
-    {readerExtensions = 
-        extensionsFromList 
-            [ Ext_tex_math_dollars
-            , Ext_superscript 
-            , Ext_subscript
-            ] 
-    }
-
--- | Read a figure caption in Markdown format.
-captionReader :: String -> Maybe [Inline]
-captionReader t = either (const Nothing) (Just . extractFromBlocks) $ runPure $ readMarkdown' (T.pack t)
-    where
-        readMarkdown' = readMarkdown readerOptions
-
-        extractFromBlocks (Pandoc _ blocks) = mconcat $ extractInlines <$> blocks
-
-        extractInlines (Plain inlines) = inlines
-        extractInlines (Para inlines) = inlines
-        extractInlines (LineBlock multiinlines) = join multiinlines
-        extractInlines _ = []
-
-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)
-
--- | Convert a FigureSpec to a Pandoc block component
-toImage :: FigureSpec -> Block
-toImage spec = head . toList $ para $ imageWith attrs' target' "fig:" caption'
-    -- To render images as figures with captions, the target title
-    -- must be "fig:"
-    -- Janky? yes
-    where
-        attrs'       = blockAttrs spec
-        target'      = figurePath spec
-        srcLink      = link (replaceExtension target' ".txt") mempty "Source code" 
-        hiresLink    = link (hiresFigurePath spec) mempty "high res."
-        captionText  = fromList $ fromMaybe mempty (captionReader $ caption spec)
-        captionLinks = mconcat [" (", srcLink, ", ", hiresLink, ")"]
-        caption'     = captionText <> captionLinks
-
--- | 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
-
--- | Determine the path to the source code that generated the figure.
-sourceCodePath :: FigureSpec -> FilePath
-sourceCodePath = flip replaceExtension ".txt" . figurePath
-
--- | 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 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,6 +1,5 @@ {-# LANGUAGE MultiWayIf        #-}
 {-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE Unsafe            #-}
 
 {-|
 Module      : Text.Pandoc.Filter.Pyplot
@@ -66,23 +65,26 @@ @
 
 -}
-module Text.Pandoc.Filter.Pyplot
-    ( makePlot
+module Text.Pandoc.Filter.Pyplot (
+    -- * Operating on single Pandoc blocks 
+      makePlot
+    , makePlotWithConfig
+    -- * Operating on whole Pandoc documents
     , plotTransform
+    , plotTransformWithConfig
+    -- * For configuration purposes
+    , configuration
+    , Configuration (..)
+    -- * For testing and internal purposes only
     , PandocPyplotError(..)
-      -- For testing purposes only
     , makePlot'
-    , directoryKey
-    , captionKey
-    , dpiKey
-    , includePathKey
-    , saveFormatKey
     ) where
 
-import           Control.Monad                 ((>=>))
+import           Control.Monad                 ((>=>), join)
 
 import           Data.List                     (intersperse)
 
+import           Data.Default.Class            (def)
 import qualified Data.Map.Strict               as Map
 import           Data.Maybe                    (fromMaybe)
 import           Data.Monoid                   ((<>))
@@ -99,12 +101,9 @@ import           Text.Pandoc.Definition
 import           Text.Pandoc.Walk              (walkM)
 
-import           Text.Pandoc.Filter.FigureSpec (FigureSpec (..),
-                                                SaveFormat (..), addPlotCapture,
-                                                figurePath, sourceCodePath, saveFormatFromString, 
-                                                toImage)
-import           Text.Pandoc.Filter.Scripting
+import           Text.Pandoc.Filter.Pyplot.Internal
 
+
 -- | Possible errors returned by the filter
 data PandocPyplotError
     = ScriptError Int    -- ^ Running Python script has yielded an error
@@ -115,52 +114,40 @@     show (ScriptError exitcode) = "Script error: plot could not be generated. Exit code " <> (show exitcode)
     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"
-captionKey     = "caption"
-dpiKey         = "dpi"
-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 @".pyplot"@ class is required, but all other parameters are optional
-parseFigureSpec :: Block -> IO (Maybe FigureSpec)
-parseFigureSpec (CodeBlock (id', cls, attrs) content)
+parseFigureSpec :: Configuration -> Block -> IO (Maybe FigureSpec)
+parseFigureSpec config (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 mempty) $ T.readFile <$> includePath
+        includeScript <- fromMaybe (return $ defaultIncludeScript config) $ 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'
+            format      = fromMaybe (defaultSaveFormat config) $ join $ saveFormatFromString <$> Map.lookup saveFormatKey attrs'
+            dir         = makeValid $ Map.findWithDefault (defaultDirectory config) directoryKey attrs'
+            dpi'        = fromMaybe (defaultDPI config) $ read <$> Map.lookup dpiKey attrs'
             blockAttrs' = (id', filter (/= "pyplot") cls, filteredAttrs)
         return $ FigureSpec caption' fullScript format dir dpi' blockAttrs'
 
-parseFigureSpec _ = return Nothing
+parseFigureSpec _ _ = return Nothing
 
 -- | 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
+runScriptIfNecessary :: Configuration -> FigureSpec -> IO ScriptResult
+runScriptIfNecessary config spec = do
     createDirectoryIfMissing True . takeDirectory $ figurePath spec
     fileAlreadyExists <- doesFileExist $ figurePath spec
     result <- if fileAlreadyExists
                 then return ScriptSuccess
-                else runTempPythonScript $ addPlotCapture spec
+                else runTempPythonScript (interpreter config) (addPlotCapture spec)
     case result of
         ScriptFailure code -> return $ ScriptFailure code
         ScriptSuccess      -> T.writeFile (sourceCodePath spec) (script spec) >> return ScriptSuccess
@@ -168,15 +155,15 @@ -- | Main routine to include Matplotlib plots.
 -- Code blocks containing the attributes @.pyplot@ are considered
 -- Python plotting scripts. All other possible blocks are ignored.
-makePlot' :: Block -> IO (Either PandocPyplotError Block)
-makePlot' block = do
-    parsed <- parseFigureSpec block  
+makePlot' :: Configuration -> Block -> IO (Either PandocPyplotError Block)
+makePlot' config block = do
+    parsed <- parseFigureSpec config block  
     case parsed of
         Nothing   -> return $ Right block
         Just spec ->
             if hasBlockingShowCall (script spec)
                 then return $ Left BlockingCallError
-                else handleResult spec <$> runScriptIfNecessary spec 
+                else handleResult spec <$> runScriptIfNecessary config spec 
     where
         handleResult _   (ScriptFailure code) = Left  $ ScriptError code
         handleResult spec ScriptSuccess       = Right $ toImage spec
@@ -185,9 +172,22 @@ -- All code blocks that have the '.pyplot' parameter will be considered
 -- figures.
 makePlot :: Block -> IO Block
-makePlot = makePlot' >=> either (fail . show) return
+makePlot = makePlotWithConfig def
 
+-- | like @makePlot@ with with a custom default values.
+--
+-- @since 2.1.0.0
+makePlotWithConfig :: Configuration -> Block -> IO Block
+makePlotWithConfig config = makePlot' config >=> either (fail . show) return
+
 -- | Walk over an entire Pandoc document, changing appropriate code blocks
--- into figures.
+-- into figures. Default configuration is used.
 plotTransform :: Pandoc -> IO Pandoc
 plotTransform = walkM makePlot
+
+-- | Walk over an entire Pandoc document, changing appropriate code blocks
+-- into figures. The default values are determined by a @Configuration@.
+--
+-- @since 2.1.0.0
+plotTransformWithConfig :: Configuration -> Pandoc -> IO Pandoc
+plotTransformWithConfig = walkM . makePlotWithConfig
+ src/Text/Pandoc/Filter/Pyplot/Configuration.hs view
@@ -0,0 +1,126 @@+{-# LANGUAGE OverloadedStrings #-}
+{-|
+Module      : Text.Pandoc.Filter.Pyplot.Configuration
+Copyright   : (c) Laurent P René de Cotret, 2019
+License     : MIT
+Maintainer  : laurent.decotret@outlook.com
+Stability   : internal
+Portability : portable
+
+Configuration for pandoc-pyplot
+-}
+
+module Text.Pandoc.Filter.Pyplot.Configuration (
+      Configuration (..)
+    , configuration
+    -- * For testing and internal purposes only
+    , inclusionKeys
+    , directoryKey
+    , captionKey
+    , dpiKey
+    , includePathKey
+    , saveFormatKey
+) where
+
+import           Data.Maybe                    (fromMaybe)
+import           Data.Default.Class            (Default, def)
+import qualified Data.Text                     as T
+import qualified Data.Text.IO                  as T
+import           Data.Yaml         
+import           Data.Yaml.Config              (loadYamlSettings, ignoreEnv)
+
+import Text.Pandoc.Filter.Pyplot.FigureSpec
+import Text.Pandoc.Filter.Pyplot.Scripting
+
+-- | 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"
+captionKey     = "caption"
+dpiKey         = "dpi"
+includePathKey = "include"
+saveFormatKey  = "format"
+
+-- | list of all keys related to pandoc-pyplot.
+inclusionKeys :: [String]
+inclusionKeys = [ directoryKey
+                , captionKey
+                , dpiKey
+                , includePathKey
+                , saveFormatKey
+                ]
+
+-- | Configuration of pandoc-pyplot, describing the default behavior
+-- of the filter. 
+--
+-- A Configuration is useful when dealing with lots of figures; it avoids
+-- repeating the same values.sta
+-- 
+-- @since 2.1.0.0
+data Configuration 
+    = Configuration 
+        { defaultDirectory     :: FilePath     -- ^ The default directory where figures will be saved.
+        , defaultIncludeScript :: PythonScript -- ^ The default script to run before other instructions.
+        , defaultSaveFormat    :: SaveFormat   -- ^ The default save format of generated figures.
+        , defaultDPI           :: Int          -- ^ The default dots-per-inch value for generated figures.
+        , interpreter          :: String       -- ^ The name of the interpreter to use to render figures.
+        }
+    deriving (Eq, Show)
+
+instance Default Configuration where
+    def = Configuration {
+          defaultDirectory     = "generated/"
+        , defaultIncludeScript = mempty
+        , defaultSaveFormat    = PNG
+        , defaultDPI           = 80
+        , interpreter          = "python"
+    }
+
+-- A @Configuration@ cannot be directly created from a YAML file
+-- for two reasons:
+--
+--     * we want to store an include script. However, it makes more sense to 
+--       specify the script path in a YAML file.
+--     * Save format is best specified by a string, and this must be parsed later 
+--
+-- Therefore, we have another type, ConfigPrecursor, which CAN be created directly from
+-- a YAML file.
+data ConfigPrecursor
+    = ConfigPrecursor
+        { defaultDirectory_   :: FilePath
+        , defaultIncludePath_ :: Maybe FilePath
+        , defaultSaveFormat_  :: String
+        , defaultDPI_         :: Int
+        , interpreter_        :: String
+        } 
+
+instance FromJSON ConfigPrecursor where
+    parseJSON (Object v) = ConfigPrecursor
+        <$> v .:? (T.pack directoryKey)  .!= (defaultDirectory def)
+        <*> v .:? (T.pack includePathKey)
+        <*> v .:? (T.pack saveFormatKey) .!= (extension $ defaultSaveFormat def)
+        <*> v .:? (T.pack dpiKey)        .!= (defaultDPI def)
+        <*> v .:? "interpreter"          .!= (interpreter def)
+    
+    parseJSON _ = fail "Could not parse the configuration"
+
+renderConfiguration :: ConfigPrecursor -> IO Configuration
+renderConfiguration prec = do
+    includeScript <- fromMaybe mempty $ T.readFile <$> defaultIncludePath_ prec
+    let saveFormat' = fromMaybe (defaultSaveFormat def) $ saveFormatFromString $ defaultSaveFormat_ prec
+    return $ Configuration { defaultDirectory     = defaultDirectory_ prec
+                           , defaultIncludeScript = includeScript
+                           , defaultSaveFormat    = saveFormat'
+                           , defaultDPI           = defaultDPI_ prec
+                           , interpreter          = interpreter_ prec
+                           }
+
+
+-- | Building configuration from a YAML file. The
+-- keys are exactly the same as for Markdown code blocks.
+--
+-- If a key is either not present or unreadable, its value will be set
+-- to the default value.
+--
+-- @since 2.1.0.0
+configuration :: FilePath -> IO Configuration
+configuration fp = loadYamlSettings [fp] [] ignoreEnv >>= renderConfiguration
+ src/Text/Pandoc/Filter/Pyplot/FigureSpec.hs view
@@ -0,0 +1,177 @@+{-# LANGUAGE OverloadedStrings #-}
+
+{-|
+Module      : Text.Pandoc.Filter.Pyplot.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.Pyplot.FigureSpec
+    ( FigureSpec(..)
+    , SaveFormat(..)
+    , saveFormatFromString
+    , toImage
+    , sourceCodePath
+    , figurePath
+    , addPlotCapture
+    -- for testing purposes
+    , extension
+    ) where
+
+import           Control.Monad                (join)
+
+import           Data.Char                    (toLower)
+import           Data.Hashable                (Hashable, hash, hashWithSalt)
+import           Data.Maybe                   (fromMaybe)
+import           Data.Monoid                  ((<>))
+import qualified Data.Text                    as T
+
+import           System.FilePath              (FilePath, addExtension,
+                                               replaceExtension, (</>))
+
+import           Text.Pandoc.Definition       
+import           Text.Pandoc.Builder          (imageWith, link, para, fromList, toList)
+
+import           Text.Pandoc.Class            (runPure)
+import           Text.Pandoc.Extensions       (extensionsFromList, Extension(..))
+import           Text.Pandoc.Options          (def, ReaderOptions(..))
+import           Text.Pandoc.Readers          (readMarkdown)
+
+import Text.Pandoc.Filter.Pyplot.Scripting
+
+readerOptions :: ReaderOptions
+readerOptions = def 
+    {readerExtensions = 
+        extensionsFromList 
+            [ Ext_tex_math_dollars
+            , Ext_superscript 
+            , Ext_subscript
+            ] 
+    }
+
+-- | Read a figure caption in Markdown format. LaTeX math @$...$@ is supported,
+-- as are Markdown subscripts and superscripts.
+captionReader :: String -> Maybe [Inline]
+captionReader t = either (const Nothing) (Just . extractFromBlocks) $ runPure $ readMarkdown' (T.pack t)
+    where
+        readMarkdown' = readMarkdown readerOptions
+
+        extractFromBlocks (Pandoc _ blocks) = mconcat $ extractInlines <$> blocks
+
+        extractInlines (Plain inlines) = inlines
+        extractInlines (Para inlines) = inlines
+        extractInlines (LineBlock multiinlines) = join multiinlines
+        extractInlines _ = []
+
+-- | Generated figure file format supported by pandoc-pyplot. 
+data SaveFormat
+    = PNG
+    | PDF
+    | SVG
+    | JPG
+    | EPS
+    | GIF
+    | TIF
+    deriving (Bounded, Enum, Eq, Show)
+
+-- | Parse an image save format string
+--
+-- >>> saveFormatFromString ".png"
+-- Just PNG
+--
+-- >>> saveFormatFromString "jpeg"
+-- Just JPEG
+--
+-- >>> SaveFormatFromString "arbitrary"
+-- Nothing
+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` ["eps", "EPS", ".eps"] = Just EPS
+    | s `elem` ["gif", "GIF", ".gif"] = Just GIF
+    | s `elem` ["jpg", "jpeg", "JPG", "JPEG", ".jpg", ".jpeg"] = Just JPG
+    | s `elem` ["tif", "tiff", "TIF", "TIFF", ".tif", ".tiff"] = Just TIF
+    | otherwise = Nothing
+
+-- | Save format file extension
+extension :: SaveFormat -> String
+extension fmt = mconcat [".", fmap toLower . show $ fmt]
+
+-- | 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
+                          , fromEnum . saveFormat $ spec
+                          , directory spec, dpi spec
+                          , blockAttrs spec
+                          )
+
+-- | Convert a FigureSpec to a Pandoc block component
+toImage :: FigureSpec -> Block
+toImage spec = head . toList $ para $ imageWith attrs' target' "fig:" caption'
+    -- To render images as figures with captions, the target title
+    -- must be "fig:"
+    -- Janky? yes
+    where
+        attrs'       = blockAttrs spec
+        target'      = figurePath spec
+        srcLink      = link (replaceExtension target' ".txt") mempty "Source code" 
+        hiresLink    = link (hiresFigurePath spec) mempty "high res."
+        captionText  = fromList $ fromMaybe mempty (captionReader $ caption spec)
+        captionLinks = mconcat [" (", srcLink, ", ", hiresLink, ")"]
+        caption'     = captionText <> captionLinks
+
+-- | 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
+
+-- | Determine the path to the source code that generated the figure.
+sourceCodePath :: FigureSpec -> FilePath
+sourceCodePath = flip replaceExtension ".txt" . figurePath
+
+-- | 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 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/Internal.hs view
@@ -0,0 +1,21 @@+
+{-|
+Module      : Text.Pandoc.Filter.Internal
+Copyright   : (c) Laurent P René de Cotret, 2019
+License     : MIT
+Maintainer  : laurent.decotret@outlook.com
+Stability   : internal
+Portability : portable
+
+This module re-exports internal pandoc-pyplot functionality.
+-}
+
+module Text.Pandoc.Filter.Pyplot.Internal (
+      module Text.Pandoc.Filter.Pyplot.Configuration
+    , module Text.Pandoc.Filter.Pyplot.FigureSpec
+    , module Text.Pandoc.Filter.Pyplot.Scripting
+ ) where
+
+import Text.Pandoc.Filter.Pyplot.Configuration
+import Text.Pandoc.Filter.Pyplot.FigureSpec
+import Text.Pandoc.Filter.Pyplot.Scripting
+ src/Text/Pandoc/Filter/Pyplot/Scripting.hs view
@@ -0,0 +1,68 @@+{-# LANGUAGE OverloadedStrings #-}
+
+{-|
+Module      : Text.Pandoc.Filter.Pyplot.Scripting
+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 running Python scripts.
+-}
+module Text.Pandoc.Filter.Pyplot.Scripting
+    ( runTempPythonScript
+    , hasBlockingShowCall
+    , PythonScript
+    , ScriptResult(..)
+    ) where
+
+import           Data.Hashable        (hash)
+import           Data.Monoid          (Any(..))
+import           Data.Text            (Text)
+import qualified Data.Text            as T
+import qualified Data.Text.IO         as T
+
+import           System.Exit          (ExitCode (..))
+import           System.FilePath      ((</>))
+import           System.IO.Temp       (getCanonicalTemporaryDirectory)
+import           System.Process.Typed (runProcess, shell)
+
+-- | String representation of a Python script
+type PythonScript = Text
+
+-- | Possible result of running a Python script
+data ScriptResult
+    = ScriptSuccess
+    | ScriptFailure Int
+
+-- | Take a python script in string form, write it in a temporary directory,
+-- then execute it.
+runTempPythonScript :: String          -- ^ Interpreter (e.g. "python" or "python35")
+                    -> PythonScript    -- ^ Content of the script
+                    -> IO ScriptResult -- ^ Result with exit code.
+runTempPythonScript interpreter script = do
+    -- We involve the script hash as a temporary filename
+    -- so that there is never any collision
+    scriptPath <- (</> hashedPath) <$> getCanonicalTemporaryDirectory
+    T.writeFile scriptPath script
+
+    ec <- runProcess $ shell $ mconcat [interpreter, " ", 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
+        [ "plt.show()" `elem` scriptLines
+        , "pyplot.show()" `elem` scriptLines
+        , "matplotlib.pyplot.show()" `elem` scriptLines
+        ]
+  where
+    scriptLines = T.lines script
+    anyOf xs = getAny $ mconcat $ Any <$> xs
− src/Text/Pandoc/Filter/Scripting.hs
@@ -1,73 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE Unsafe            #-}
-
-{-|
-Module      : Text.Pandoc.Filter.Scripting
-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 running Python scripts.
--}
-module Text.Pandoc.Filter.Scripting
-    ( runTempPythonScript
-    , hasBlockingShowCall
-    , PythonScript
-    , ScriptResult(..)
-    ) where
-
-import           Data.Text            (Text)
-import qualified Data.Text            as T
-import qualified Data.Text.IO         as T
-
-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 = Text
-
--- | Possible result of running a Python script
-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
-    -- 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
-        [ "plt.show()" `elem` scriptLines
-        , "pyplot.show()" `elem` scriptLines
-        , "matplotlib.pyplot.show()" `elem` scriptLines
-        ]
-  where
-    scriptLines = T.lines script
-    anyOf xs = getAny $ mconcat $ Any <$> xs
stack.yaml view
@@ -38,7 +38,7 @@ # using the same syntax as the packages field.
 # (e.g., acme-missiles-0.3)
 extra-deps:
-- hindent-5.2.7@sha256:a0a90f7cc160eeba3816e53f310e16942e68d9a09fe092d9835ea6bbbcd2cd91
+- yaml-config-0.4.0
 
 # Override default flag values for local packages and extra-deps
 # flags: {}
test/Main.hs view
@@ -5,15 +5,16 @@ 
 import           Control.Monad                 (unless)
 
-import           Data.List                     (isInfixOf)
+import           Data.Default.Class            (def)
+import           Data.List                     (isInfixOf, isSuffixOf)
+import           Data.Monoid                   ((<>))
 import           Data.Text                     (unpack)
 
 import           Test.Tasty
 import           Test.Tasty.HUnit
 
-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.Filter.Pyplot
+import           Text.Pandoc.Filter.Pyplot.Internal
 
 import           Text.Pandoc.JSON
 import qualified Text.Pandoc.Builder           as B
@@ -25,7 +26,7 @@                                                 doesFileExist, listDirectory,
                                                 removeDirectoryRecursive,
                                                 removePathForcibly)
-import           System.FilePath               (isExtensionOf, (</>))
+import           System.FilePath               ((</>), takeExtensions)
 import           System.IO.Temp                (getCanonicalTemporaryDirectory)
 
 main :: IO ()
@@ -33,30 +34,38 @@     defaultMain $
     testGroup
         "Text.Pandoc.Filter.Pyplot"
-        [testFileCreation, testFileInclusion, testSaveFormat, testBlockingCallError, testMarkdownFormattingCaption]
+        [ testFileCreation
+        , testFileInclusion
+        , testSaveFormat
+        , testBlockingCallError
+        , testMarkdownFormattingCaption
+        , testWithConfiguration
+        , testOverridingConfiguration
+        , testBuildConfiguration
+        ]
 
-plotCodeBlock :: P.PythonScript -> Block
+plotCodeBlock :: 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
+    CodeBlock (id', cls, attrs ++ [(captionKey, caption)]) script
 
 addDirectory :: FilePath -> Block -> Block
 addDirectory dir (CodeBlock (id', cls, attrs) script) =
-    CodeBlock (id', cls, attrs ++ [(P.directoryKey, dir)]) script
+    CodeBlock (id', cls, attrs ++ [(directoryKey, dir)]) script
 
 addInclusion :: FilePath -> Block -> Block
 addInclusion inclusionPath (CodeBlock (id', cls, attrs) script) =
-    CodeBlock (id', cls, attrs ++ [(P.includePathKey, inclusionPath)]) script
+    CodeBlock (id', cls, attrs ++ [(includePathKey, inclusionPath)]) script
 
-addSaveFormat :: P.SaveFormat -> Block -> Block
+addSaveFormat :: SaveFormat -> Block -> Block
 addSaveFormat saveFormat (CodeBlock (id', cls, attrs) script) =
-    CodeBlock (id', cls, attrs ++ [(P.saveFormatKey, P.extension saveFormat)]) script
+    CodeBlock (id', cls, attrs ++ [(saveFormatKey, extension saveFormat)]) script
 
 addDPI :: Int -> Block -> Block
 addDPI dpi (CodeBlock (id', cls, attrs) script) =
-    CodeBlock (id', cls, attrs ++ [(P.dpiKey, show dpi)]) script
+    CodeBlock (id', cls, attrs ++ [(dpiKey, show dpi)]) script
 
 -- | Assert that a file exists
 assertFileExists :: HasCallStack => FilePath -> Assertion
@@ -66,6 +75,14 @@   where
     msg = mconcat ["File ", filepath, " does not exist."]
 
+-- | Not available with GHC < 8.4
+-- since this function was added in filepath-1.4.2
+-- but GHC 8.2.2 comes with filepath-1.4.1.2
+isExtensionOf :: String -> FilePath -> Bool
+isExtensionOf ext@('.':_) = isSuffixOf ext . takeExtensions
+isExtensionOf ext         = isSuffixOf ('.':ext) . takeExtensions
+    
+
 -- | Assert that the first list is contained,
 -- wholly and intact, anywhere within the second.
 assertIsInfix :: (Eq a, Show a, HasCallStack) => [a] -> [a] -> Assertion
@@ -90,7 +107,7 @@         tempDir <- (</> "test-file-creation") <$> getCanonicalTemporaryDirectory
         ensureDirectoryExistsAndEmpty tempDir
         let codeBlock = (addDirectory tempDir $ plotCodeBlock "import matplotlib.pyplot as plt\n")
-        _ <- P.makePlot' codeBlock
+        _ <- makePlot' def codeBlock
         filesCreated <- length <$> listDirectory tempDir
         assertEqual "" filesCreated 3
 
@@ -104,7 +121,7 @@         let codeBlock =
                 (addInclusion "test/fixtures/include.py" $
                  addDirectory tempDir $ plotCodeBlock "import matplotlib.pyplot as plt\n")
-        _ <- P.makePlot' codeBlock
+        _ <- makePlot' def codeBlock
         inclusion <- readFile "test/fixtures/include.py"
         sourcePath <- head . filter (isExtensionOf "txt") <$> listDirectory tempDir
         src <- readFile (tempDir </> sourcePath)
@@ -118,13 +135,13 @@         tempDir <- (</> "test-safe-format") <$> getCanonicalTemporaryDirectory
         ensureDirectoryExistsAndEmpty tempDir
         let codeBlock =
-                (addSaveFormat P.JPG $
+                (addSaveFormat JPG $
                  addDirectory tempDir $
                  plotCodeBlock
                      "import matplotlib.pyplot as plt\nplt.figure()\nplt.plot([1,2], [1,2])")
-        _ <- P.makePlot' codeBlock
+        _ <- makePlot' def codeBlock
         numberjpgFiles <-
-            length <$> filter (isExtensionOf (P.extension P.JPG)) <$>
+            length <$> filter (isExtensionOf (extension JPG)) <$>
             listDirectory tempDir
         assertEqual "" numberjpgFiles 2
 
@@ -136,11 +153,11 @@     testCase "raises an exception for blocking calls" $ do
         tempDir <- (</> "test-blocking-call-error") <$>getCanonicalTemporaryDirectory
         let codeBlock = plotCodeBlock "import matplotlib.pyplot as plt\nplt.show()"
-        result <- P.makePlot' codeBlock
+        result <- makePlot' def codeBlock
         case result of
             Right block -> assertFailure "did not catch the expected blocking call"
             Left error ->
-                if error == P.BlockingCallError
+                if error == BlockingCallError
                     then pure ()
                     else assertFailure "did not catch the expected blocking call"
 -------------------------------------------------------------------------------
@@ -151,11 +168,12 @@ testMarkdownFormattingCaption =
     testCase "appropriately parses Markdown captions" $ do
         tempDir <- (</> "test-caption-parsing") <$> getCanonicalTemporaryDirectory
+        ensureDirectoryExistsAndEmpty tempDir
         -- Note that this test is fragile, in the sense that the expected result must be carefully
         -- constructed
         let expected = [B.Strong [B.Str "caption"]]
             codeBlock = addDirectory tempDir $ addCaption "**caption**" $ plotCodeBlock "import matplotlib.pyplot as plt"
-        result <- P.makePlot' codeBlock
+        result <- makePlot' def codeBlock
         case result of
             Left error -> assertFailure $ "an error occured: " <> show error
             Right block -> assertIsInfix expected (extractCaption block)
@@ -166,3 +184,59 @@         extractImageCaption (Image _ c _) = c
         extractImageCaption _ = mempty
 -------------------------------------------------------------------------------
+
+-------------------------------------------------------------------------------
+-- Test with configuration
+testConfig :: IO Configuration
+testConfig = do
+    tempDir <- (</> "test-with-config") <$> getCanonicalTemporaryDirectory
+    ensureDirectoryExistsAndEmpty tempDir
+    return $ def {defaultDirectory = tempDir, defaultSaveFormat = JPG}
+
+testOverridingConfiguration :: TestTree
+testOverridingConfiguration =
+    testCase "follows the configuration options" $ do
+        config <- testConfig
+
+        -- The default from config says the save format should be JPG
+        -- but the code block save format="png"
+        let codeBlock = (addSaveFormat PNG $ 
+                         plotCodeBlock 
+                            "import matplotlib.pyplot as plt\nplt.figure()\nplt.plot([1,2], [1,2])")
+        _ <- makePlot' config codeBlock
+
+        numberjpgFiles <-
+            length <$> filter (isExtensionOf (extension JPG)) <$>
+            listDirectory (defaultDirectory config)
+        numberpngFiles <-
+            length <$> filter (isExtensionOf (extension PNG)) <$>
+            listDirectory (defaultDirectory config)
+        assertEqual "" numberjpgFiles 0
+        assertEqual "" numberpngFiles 2
+-------------------------------------------------------------------------------
+
+-------------------------------------------------------------------------------
+-- Test that values in code blocks will override the defaults in configuration
+testWithConfiguration :: TestTree
+testWithConfiguration =
+    testCase "code block attributes override configuration defaults" $ do
+        config <- testConfig
+
+        let codeBlock = plotCodeBlock "import matplotlib.pyplot as plt\nplt.figure()\nplt.plot([1,2], [1,2])"
+        _ <- makePlot' config codeBlock
+
+        numberjpgFiles <-
+            length <$> filter (isExtensionOf (extension JPG)) <$>
+            listDirectory (defaultDirectory config)
+        assertEqual "" numberjpgFiles 2
+-------------------------------------------------------------------------------
+
+testBuildConfiguration :: TestTree
+testBuildConfiguration = 
+    testCase "configuration is correctly parsed" $ do
+        let config = def { defaultDirectory = "generated/other"
+                         , defaultSaveFormat = JPG
+                         , defaultDPI = 150
+                         }
+        parsedConfig <- configuration "test/fixtures/.pandoc-pyplot.yml"
+        assertEqual "" config parsedConfig