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 +75/−65
- LICENSE.md +7/−9
- README.md +32/−6
- executable/Main.hs +20/−4
- pandoc-pyplot.cabal +10/−3
- src/Text/Pandoc/Filter/FigureSpec.hs +0/−158
- src/Text/Pandoc/Filter/Pyplot.hs +43/−43
- src/Text/Pandoc/Filter/Pyplot/Configuration.hs +126/−0
- src/Text/Pandoc/Filter/Pyplot/FigureSpec.hs +177/−0
- src/Text/Pandoc/Filter/Pyplot/Internal.hs +21/−0
- src/Text/Pandoc/Filter/Pyplot/Scripting.hs +68/−0
- src/Text/Pandoc/Filter/Scripting.hs +0/−73
- stack.yaml +1/−1
- test/Main.hs +95/−21
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 -[](http://hackage.haskell.org/package/pandoc-pyplot) [](http://stackage.org/nightly/package/pandoc-pyplot) [](http://stackage.org/nightly/package/pandoc-pyplot) [](https://ci.appveyor.com/project/LaurentRDC/pandoc-pyplot)  +[](http://hackage.haskell.org/package/pandoc-pyplot) [](http://stackage.org/nightly/package/pandoc-pyplot) [](http://stackage.org/nightly/package/pandoc-pyplot) [](https://ci.appveyor.com/project/LaurentRDC/pandoc-pyplot)  `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