pandoc-pyplot 2.1.5.1 → 2.2.0.0
raw patch · 12 files changed
+401/−281 lines, 12 filesdep +mtldep +shakespearedep ~yamlPVP ok
version bump matches the API change (PVP)
Dependencies added: mtl, shakespeare
Dependency ranges changed: yaml
API changes (from Hackage documentation)
+ Text.Pandoc.Filter.Pyplot.Internal: Matplotlib :: RenderingLibrary
+ Text.Pandoc.Filter.Pyplot.Internal: Plotly :: RenderingLibrary
+ Text.Pandoc.Filter.Pyplot.Internal: [renderingLib] :: FigureSpec -> RenderingLibrary
+ Text.Pandoc.Filter.Pyplot.Internal: data RenderingLibrary
+ Text.Pandoc.Filter.Pyplot.Internal: type PyplotM a = ReaderT Configuration IO a
- Text.Pandoc.Filter.Pyplot: makePlot' :: Configuration -> Block -> IO (Either PandocPyplotError Block)
+ Text.Pandoc.Filter.Pyplot: makePlot' :: Block -> PyplotM (Either PandocPyplotError Block)
- Text.Pandoc.Filter.Pyplot.Internal: FigureSpec :: String -> Bool -> PythonScript -> SaveFormat -> FilePath -> Int -> Bool -> Bool -> Attr -> FigureSpec
+ Text.Pandoc.Filter.Pyplot.Internal: FigureSpec :: String -> Bool -> PythonScript -> SaveFormat -> FilePath -> Int -> RenderingLibrary -> Bool -> Bool -> Attr -> FigureSpec
- Text.Pandoc.Filter.Pyplot.Internal: parseFigureSpec :: Configuration -> Block -> IO (Maybe FigureSpec)
+ Text.Pandoc.Filter.Pyplot.Internal: parseFigureSpec :: Block -> PyplotM (Maybe FigureSpec)
- Text.Pandoc.Filter.Pyplot.Internal: runScriptIfNecessary :: Configuration -> FigureSpec -> IO ScriptResult
+ Text.Pandoc.Filter.Pyplot.Internal: runScriptIfNecessary :: FigureSpec -> PyplotM ScriptResult
- Text.Pandoc.Filter.Pyplot.Internal: runTempPythonScript :: String -> [String] -> PythonScript -> IO ScriptResult
+ Text.Pandoc.Filter.Pyplot.Internal: runTempPythonScript :: PythonScript -> PyplotM ScriptResult
Files
- CHANGELOG.md +5/−0
- README.md +44/−16
- executable/Main.hs +27/−25
- pandoc-pyplot.cabal +6/−3
- src/Text/Pandoc/Filter/Pyplot.hs +25/−23
- src/Text/Pandoc/Filter/Pyplot/Configuration.hs +38/−35
- src/Text/Pandoc/Filter/Pyplot/FigureSpec.hs +110/−65
- src/Text/Pandoc/Filter/Pyplot/Internal.hs +5/−5
- src/Text/Pandoc/Filter/Pyplot/Scripting.hs +42/−33
- src/Text/Pandoc/Filter/Pyplot/Types.hs +61/−40
- stack.yaml +1/−1
- test/Main.hs +37/−35
CHANGELOG.md view
@@ -2,6 +2,11 @@ pandoc-pyplot uses [Semantic Versioning](http://semver.org/spec/v2.0.0.html) +Release 2.2.0.0 +--------------- + +* Added support for rendering figures via the Plotly library + Release 2.1.5.1 ---------------
README.md view
@@ -1,9 +1,9 @@-# pandoc-pyplot - A Pandoc filter to generate Matplotlib figures directly in documents +# pandoc-pyplot - A Pandoc filter to generate Matplotlib/Plotly 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) [](https://dev.azure.com/laurentdecotret/pandoc-pyplot/_build/latest?definitionId=2&branchName=master) +[](http://hackage.haskell.org/package/pandoc-pyplot) [](http://stackage.org/nightly/package/pandoc-pyplot) [](https://ci.appveyor.com/project/LaurentRDC/pandoc-pyplot) [](https://dev.azure.com/laurentdecotret/pandoc-pyplot/_build/latest?definitionId=2&branchName=master)  -`pandoc-pyplot` turns Python code present in your documents into embedded Matplotlib figures. +`pandoc-pyplot` turns Python code present in your documents into embedded figures via Matplotlib or Plotly. * [Usage](#usage) * [Markdown](#markdown) @@ -14,6 +14,7 @@ * [Link to source code and high-resolution figure](#link-to-source-code-and-high-resolution-figure) * [Including scripts](#including-scripts) + * [Multiple backends](#multiple-backends) * [No wasted work](#no-wasted-work) * [Compatibility with pandoc-crossref](#compatibility-with-pandoc-crossref) @@ -29,7 +30,7 @@ ### Markdown -The filter recognizes code blocks with the `.pyplot` class present in Markdown documents. 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` or `.plotly` classes present in Markdown documents. It will run the script in the associated code block in a Python interpreter and capture the generated Matplotlib/Plotly figure. Here is a basic example using the scripting `matplotlib.pyplot` API: @@ -175,18 +176,45 @@ 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. -Customization of figures beyond what is available in `pandoc-pyplot` can also be done through the `include` script. For example, if you wanted to have transparent figures, you can do so via `matplotlib.pyplot.rcParams`: +Customization of figures beyond what is available in `pandoc-pyplot` can also be done through the `include` script. For example, if you wanted to figures with a black background, you can do so via `matplotlib.pyplot.rcParams`: ```python import matplotlib.pyplot as plt -plt.rcParams['savefig.transparent'] = True +plt.rcParams['savefig.facecolor'] = 'k' ... ``` You can take a look at all available `matplotlib` parameters [here](https://matplotlib.org/users/customizing.html). +### Multiple backends + +(*new in version 2.2.0.0*) Both Matplotlib and Plotly are supported! + +To render Plotly figures in Markdown: + +~~~markdown +```{.plotly caption="This is a Plotly figure"} +import plotly.graph_objects as go +figure = go.Figure( + data=[go.Bar(y=[2, 1, 3])], +) +~~~ + +Here is the LaTeX equivalent: + +```latex +\begin{minted}[caption=This is a Plotly figure]{plotly} +import plotly.graph_objects as go +figure = go.Figure( + data=[go.Bar(y=[2, 1, 3])], +) +\end{minted} +``` + +`pandoc-pyplot` will render and capture your figure automagically. + ### No wasted work -`pandoc-pyplot` minimizes work, only generating figures if it absolutely must. Therefore, you can confidently run the filter on very large documents containing dozens of figures --- like a book or a thesis --- and only the figures which have recently changed will be re-generated. +`pandoc-pyplot` minimizes work, only generating figures if it absolutely must. Therefore, you can confidently run the filter on very large documents containing dozens of figures --- like a book or a thesis --- and only the figures which have changed will be re-generated. ### Compatibility with pandoc-crossref @@ -219,9 +247,9 @@ include: mystyle.py format: jpeg links: false -dpi: 150 -tight_bbox: true -transparent: false +dpi: 150 # Matplotlib only +tight_bbox: true # Matplotlib only +transparent: false # Matplotlib only flags: [-O, -Wignore] ``` @@ -250,8 +278,8 @@ * `interpreter` is the name of the interpreter to use. For example, `interpreter: python36`; * `flags` is a list of strings, which are flags that are passed to the python interpreter. For example, `flags: [-O, -Wignore]`; -* (*New in version 2.1.5.0*) `tight_bbox` is a boolean that determines whether to use `bbox_inches="tight"` or not when saving Matplotlib figures. For example, `tight_bbox: true`. See [here](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.savefig.html) for details; -* (*New in version 2.1.5.0*) `transparent` is a boolean that determines whether to make figure background transparent or not. This is useful, for example, for displaying a plot on top of a colored background on a web page. High-resolution figures are not affected. For example, `transparent: true`. +* (*New in version 2.1.5.0*) `tight_bbox` is a boolean that determines whether to use `bbox_inches="tight"` or not when saving Matplotlib figures. For example, `tight_bbox: true`. See [here](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.savefig.html) for details. This is ignored for Plotly figures. +* (*New in version 2.1.5.0*) `transparent` is a boolean that determines whether to make Matplotlib figure background transparent or not. This is useful, for example, for displaying a plot on top of a colored background on a web page. High-resolution figures are not affected. For example, `transparent: true`. This is ignored for Plotly figures. ## Installation @@ -295,15 +323,15 @@ ### Requirements -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 on Windows, and `"python3"` otherwise. +This filter requires a Python interpreter and at least [Matplotlib](https://matplotlib.org/) or [Plotly](https://plot.ly/python/) 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 on Windows, and `"python3"` otherwise. -You can use the filter with Pandoc as follows: +Use the filter with Pandoc as follows: ```bash pandoc --filter pandoc-pyplot input.md --output output.html ``` -In which case, the output is HTML. Another example with PDF output: +in which case, the output is HTML. Another example with PDF output: ```bash pandoc --filter pandoc-pyplot input.md --output output.pdf @@ -319,7 +347,7 @@ ## Usage as a Haskell library -To include the functionality of `pandoc-pyplot` in a Haskell package, you can use the `makePlot :: Block -> IO Block` function (for single blocks) or `plotTransform :: Pandoc -> IO Pandoc` function (for entire documents). +To include the functionality of `pandoc-pyplot` in a Haskell package, you can use the `makePlot :: Block -> IO Block` function (for single blocks) or `plotTransform :: Pandoc -> IO Pandoc` function (for entire documents). Variations of these functions exist for more advanced configurations. [Take a look at the documentation on Hackage](https://hackage.haskell.org/package/pandoc-pyplot). ### Usage with Hakyll
executable/Main.hs view
@@ -1,33 +1,35 @@+{-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE ApplicativeDo #-} module Main where -import Control.Applicative ((<|>)) -import Control.Monad (join) +import Control.Applicative ((<|>)) +import Control.Monad (join) -import Data.Default.Class (def) -import Data.List (intersperse) -import Data.Monoid ((<>)) -import qualified Data.Text as T +import Data.Default.Class (def) +import Data.List (intersperse) +import Data.Monoid ((<>)) +import qualified Data.Text as T import Options.Applicative -import qualified Options.Applicative.Help.Pretty as P +import qualified Options.Applicative.Help.Pretty as P -import System.Directory (doesFileExist) -import System.IO.Temp (writeSystemTempFile) +import System.Directory (doesFileExist) +import System.IO.Temp (writeSystemTempFile) -import Text.Pandoc.Filter.Pyplot (plotTransformWithConfig, configuration, SaveFormat(..)) +import Text.Pandoc.Filter.Pyplot (SaveFormat (..), + configuration, + plotTransformWithConfig) import Text.Pandoc.Filter.Pyplot.Internal (writeConfig) -import Text.Pandoc.JSON (toJSONFilter) +import Text.Pandoc.JSON (toJSONFilter) -import Web.Browser (openBrowser) +import Web.Browser (openBrowser) -import qualified Data.Version as V -import Paths_pandoc_pyplot (version) +import qualified Data.Version as V +import Paths_pandoc_pyplot (version) -import ManPage (embedManualHtml) +import ManPage (embedManualHtml) main :: IO () main = join $ execParser opts @@ -38,8 +40,8 @@ <> header "pandoc-pyplot - generate Matplotlib figures directly in documents." <> footerDoc (Just footer') ) - + toJSONFilterWithConfig :: IO () toJSONFilterWithConfig = do configExists <- doesFileExist ".pandoc-pyplot.yml" @@ -58,28 +60,28 @@ run :: Parser (IO ()) run = do - versionP <- flag Nothing (Just Version) (long "version" <> short 'v' + versionP <- flag Nothing (Just Version) (long "version" <> short 'v' <> help "Show version number and exit.") - formatsP <- flag Nothing (Just Formats) (long "formats" <> short 'f' + formatsP <- flag Nothing (Just Formats) (long "formats" <> short 'f' <> help "Show supported output figure formats and exit.") - manualP <- flag Nothing (Just Manual) (long "manual" <> short 'm' + manualP <- flag Nothing (Just Manual) (long "manual" <> short 'm' <> help "Open the manual page in the default web browser and exit.") - configP <- flag Nothing (Just Config) (long "write-example-config" + configP <- flag Nothing (Just Config) (long "write-example-config" <> help "Write the default configuration in '.pandoc-pyplot.yml', \ \which you can subsequently customize, and exit. If '.pandoc-pyplot.yml' \ \already exists, an error will be thrown. ") - + input <- optional $ strArgument (metavar "AST") return $ go (versionP <|> formatsP <|> manualP <|> configP) input where go :: Maybe Flag -> Maybe String -> IO () go (Just Version) _ = putStrLn (V.showVersion version) go (Just Formats) _ = putStrLn . mconcat . intersperse ", " . fmap show $ supportedSaveFormats - go (Just Manual) _ = writeSystemTempFile "pandoc-pyplot-manual.html" (T.unpack manualHtml) - >>= \fp -> openBrowser ("file:///" <> fp) + go (Just Manual) _ = writeSystemTempFile "pandoc-pyplot-manual.html" (T.unpack manualHtml) + >>= \fp -> openBrowser ("file:///" <> fp) >> return () go (Just Config) _ = writeConfig ".pandoc-pyplot.yml" def go Nothing _ = toJSONFilterWithConfig @@ -108,4 +110,4 @@ , P.line , P.indent 4 $ P.text "https://github.com/LaurentRDC/pandoc-pyplot" , P.line - ]+ ]
pandoc-pyplot.cabal view
@@ -1,8 +1,8 @@ name: pandoc-pyplot -version: 2.1.5.1 +version: 2.2.0.0 cabal-version: >= 1.12 synopsis: A Pandoc filter to include figures generated from Python code blocks -description: A Pandoc filter to include figures generated from Python code blocks. Keep the document and Python code in the same location. Output from Matplotlib is captured and included as a figure. +description: A Pandoc filter to include figures generated from Python code blocks. Keep the document and Python code in the same location. Output is captured and included as a figure. category: Documentation homepage: https://github.com/LaurentRDC/pandoc-pyplot#readme bug-reports: https://github.com/LaurentRDC/pandoc-pyplot/issues @@ -44,10 +44,12 @@ , hashable >= 1 && < 2 , pandoc >= 2 && < 3 , pandoc-types >=1.12 && < 2 + , shakespeare >= 2.0 && < 3 , temporary , text >= 1 && < 2 , typed-process >= 0.2.1 && < 1 - , yaml >= 0.8.16 + , yaml >= 0.8 && < 1 + , mtl >= 2.2 && < 2.3 default-language: Haskell2010 executable pandoc-pyplot @@ -91,5 +93,6 @@ , tasty-hspec , temporary , text + , mtl >= 2.2 && < 2.3 default-language: Haskell2010
src/Text/Pandoc/Filter/Pyplot.hs view
@@ -2,42 +2,43 @@ {-# LANGUAGE OverloadedStrings #-} {-| -Module : Text.Pandoc.Filter.Pyplot -Description : Pandoc filter to create Matplotlib figures from code blocks +Module : $header$ +Description : Pandoc filter to create Matplotlib/Plotly figures from code blocks Copyright : (c) Laurent P René de Cotret, 2019 License : MIT Maintainer : laurent.decotret@outlook.com Stability : stable Portability : portable -This module defines a Pandoc filter @makePlot@ and related functions +This module defines a Pandoc filter @makePlot@ and related functions that can be used to walk over a Pandoc document and generate figures from Python code blocks. -The syntax for code blocks is simple, Code blocks with the @.pyplot@ +The syntax for code blocks is simple, Code blocks with the @.pyplot@ or @.plotly@ attribute will trigger the filter. The code block will be reworked into a Python script and the output figure will be captured, along with a high-resolution version of the figure and the source code used to generate the figure. -To trigger pandoc-pyplot, the following is __required__: +To trigger pandoc-pyplot, one of the following is __required__: - * @.pyplot@: Trigger pandoc-pyplot but let it decide on a filename + * @.pyplot@: Trigger pandoc-pyplot, rendering via the Matplotlib library + * @.plotly@: Trigger pandoc-pyplot, rendering via the Plotly library Here are the possible attributes what pandoc-pyplot understands: * @directory=...@ : Directory where to save the figure. * @format=...@: Format of the generated figure. This can be an extension or an acronym, e.g. @format=png@. * @caption="..."@: Specify a plot caption (or alternate text). Captions support Markdown formatting and LaTeX math (@$...$@). - * @dpi=...@: Specify a value for figure resolution, or dots-per-inch. Default is 80DPI. + * @dpi=...@: Specify a value for figure resolution, or dots-per-inch. Default is 80DPI. (Matplotlib only, ignored otherwise) * @include=...@: Path to a Python script to include before the code block. Ideal to avoid repetition over many figures. - * @links=true|false@: Add links to source code and high-resolution version of this figure. + * @links=true|false@: Add links to source code and high-resolution version of this figure. This is @true@ by default, but you may wish to disable this for PDF output. - -Custom configurations are possible via the @Configuration@ type and the filter -functions @plotTransformWithConfig@ and @makePlotWithConfig@. + +Custom configurations are possible via the @Configuration@ type and the filter +functions @plotTransformWithConfig@ and @makePlotWithConfig@. -} module Text.Pandoc.Filter.Pyplot ( - -- * Operating on single Pandoc blocks + -- * Operating on single Pandoc blocks makePlot , makePlotWithConfig -- * Operating on whole Pandoc documents @@ -53,25 +54,25 @@ , makePlot' ) where -import Control.Monad ((>=>)) +import Control.Monad.Reader -import Data.Default.Class (def) +import Data.Default.Class (def) import Text.Pandoc.Definition -import Text.Pandoc.Walk (walkM) +import Text.Pandoc.Walk (walkM) import Text.Pandoc.Filter.Pyplot.Internal -- | Main routine to include Matplotlib plots. -- Code blocks containing the attributes @.pyplot@ are considered -- Python plotting scripts. All other possible blocks are ignored. -makePlot' :: Configuration -> Block -> IO (Either PandocPyplotError Block) -makePlot' config block = do - parsed <- parseFigureSpec config block - case parsed of - Nothing -> return $ Right block - Just spec -> handleResult spec <$> runScriptIfNecessary config spec - +makePlot' :: Block -> PyplotM (Either PandocPyplotError Block) +makePlot' block = do + parsed <- parseFigureSpec block + maybe + (return $ Right block) + (\s -> handleResult s <$> runScriptIfNecessary s) + parsed where handleResult _ (ScriptChecksFailed msg) = Left $ ScriptChecksFailedError msg handleResult _ (ScriptFailure code) = Left $ ScriptError code @@ -87,7 +88,8 @@ -- -- @since 2.1.0.0 makePlotWithConfig :: Configuration -> Block -> IO Block -makePlotWithConfig config = makePlot' config >=> either (fail . show) return +makePlotWithConfig config block = + runReaderT (makePlot' block >>= either (fail . show) return) config -- | Walk over an entire Pandoc document, changing appropriate code blocks -- into figures. Default configuration is used.
src/Text/Pandoc/Filter/Pyplot/Configuration.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE OverloadedStrings #-} {-| -Module : Text.Pandoc.Filter.Pyplot.Configuration +Module : $header$ Copyright : (c) Laurent P René de Cotret, 2019 License : MIT Maintainer : laurent.decotret@outlook.com @@ -25,23 +25,23 @@ , isTransparentKey ) where -import Data.Maybe (fromMaybe) -import Data.Default.Class (def) -import qualified Data.Text as T -import qualified Data.Text.IO as T -import Data.Yaml -import Data.Yaml.Config (loadYamlSettings, ignoreEnv) +import Data.Default.Class (def) +import Data.Maybe (fromMaybe) +import qualified Data.Text as T +import qualified Data.Text.IO as T +import Data.Yaml +import Data.Yaml.Config (ignoreEnv, loadYamlSettings) -import System.Directory (doesFileExist) +import System.Directory (doesFileExist) -import Text.Pandoc.Filter.Pyplot.Types +import Text.Pandoc.Filter.Pyplot.Types --- A @Configuration@ cannot be directly created from a YAML file +-- | 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 +-- * 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 +-- * 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. @@ -56,36 +56,39 @@ , transparent_ :: Bool , interpreter_ :: String , flags_ :: [String] - } + } instance FromJSON ConfigPrecursor where - parseJSON (Object v) = ConfigPrecursor - <$> v .:? (T.pack directoryKey) .!= (defaultDirectory def) - <*> v .:? (T.pack includePathKey) - <*> v .:? (T.pack withLinksKey) .!= (defaultWithLinks def) - <*> v .:? (T.pack saveFormatKey) .!= (extension $ defaultSaveFormat def) - <*> v .:? (T.pack dpiKey) .!= (defaultDPI def) - <*> v .:? (T.pack isTightBboxKey) .!= (isTightBbox def) - <*> v .:? (T.pack isTransparentKey) .!= (isTransparent def) - <*> v .:? "interpreter" .!= (interpreter def) - <*> v .:? "flags" .!= (flags def) - + parseJSON (Object v) = + ConfigPrecursor + <$> v .:? (T.pack directoryKey) .!= (defaultDirectory def) + <*> v .:? (T.pack includePathKey) + <*> v .:? (T.pack withLinksKey) .!= (defaultWithLinks def) + <*> v .:? (T.pack saveFormatKey) .!= (extension $ defaultSaveFormat def) + <*> v .:? (T.pack dpiKey) .!= (defaultDPI def) + <*> v .:? (T.pack isTightBboxKey) .!= (isTightBbox def) + <*> v .:? (T.pack isTransparentKey) .!= (isTransparent def) + <*> v .:? "interpreter" .!= (interpreter def) + <*> v .:? "flags" .!= (flags 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' - , defaultWithLinks = defaultWithLinks_ prec - , defaultDPI = defaultDPI_ prec - , isTightBbox = tightBbox_ prec - , isTransparent = transparent_ prec - , interpreter = interpreter_ prec - , flags = flags_ prec - } + return $ Configuration + { defaultDirectory = defaultDirectory_ prec + , defaultIncludeScript = includeScript + , defaultSaveFormat = saveFormat' + , defaultWithLinks = defaultWithLinks_ prec + , defaultDPI = defaultDPI_ prec + , isTightBbox = tightBbox_ prec + , isTransparent = transparent_ prec + , interpreter = interpreter_ prec + , flags = flags_ prec + } -- | Building configuration from a YAML file. The @@ -107,4 +110,4 @@ fileExists <- doesFileExist fp if fileExists then error $ mconcat ["File ", fp, " already exists."] - else encodeFile fp config+ else encodeFile fp config
src/Text/Pandoc/Filter/Pyplot/FigureSpec.hs view
@@ -1,7 +1,10 @@-{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeSynonymInstances #-} {-| -Module : Text.Pandoc.Filter.Pyplot.FigureSpec +Module : $header$ Copyright : (c) Laurent P René de Cotret, 2019 License : MIT Maintainer : laurent.decotret@outlook.com @@ -24,48 +27,59 @@ , extension ) where -import Control.Monad (join) +import Control.Monad (join) +import Control.Monad.IO.Class (liftIO) +import Control.Monad.Reader -import Data.Default.Class (def) -import Data.Hashable (hash) -import Data.List (intersperse) -import qualified Data.Map.Strict as Map -import Data.Maybe (fromMaybe) -import Data.Monoid ((<>)) -import qualified Data.Text as T -import qualified Data.Text.IO as T -import Data.Version (showVersion) +import Data.Default.Class (def) +import Data.Hashable (hash) +import Data.List (intersperse) +import qualified Data.Map.Strict as Map +import Data.Maybe (fromMaybe) +import Data.Monoid ((<>)) +import qualified Data.Text as T +import qualified Data.Text.IO as T +import Data.Version (showVersion) -import Paths_pandoc_pyplot (version) +import Paths_pandoc_pyplot (version) -import System.FilePath (FilePath, addExtension, - replaceExtension, (</>), makeValid) +import System.FilePath (FilePath, addExtension, + makeValid, normalise, + replaceExtension, (</>)) -import Text.Pandoc.Definition -import Text.Pandoc.Builder (imageWith, link, para, fromList, toList) +import Text.Pandoc.Builder (fromList, imageWith, link, + para, toList) +import Text.Pandoc.Definition +import Text.Shakespeare.Text (st) -import Text.Pandoc.Class (runPure) -import Text.Pandoc.Extensions (extensionsFromList, Extension(..)) -import Text.Pandoc.Options (ReaderOptions(..)) -import Text.Pandoc.Readers (readMarkdown) +import Text.Pandoc.Class (runPure) +import Text.Pandoc.Extensions (Extension (..), + extensionsFromList) +import Text.Pandoc.Options (ReaderOptions (..)) +import Text.Pandoc.Readers (readMarkdown) -import Text.Pandoc.Filter.Pyplot.Types +import Text.Pandoc.Filter.Pyplot.Types -- | Determine inclusion specifications from Block attributes. --- Note that the @".pyplot"@ class is required, but all other parameters are optional -parseFigureSpec :: Configuration -> Block -> IO (Maybe FigureSpec) -parseFigureSpec config (CodeBlock (id', cls, attrs) content) - | "pyplot" `elem` cls = Just <$> figureSpec +-- Note that the @".pyplot"@ OR @.plotly@ class is required, but all other +-- parameters are optional. +parseFigureSpec :: Block -> PyplotM (Maybe FigureSpec) +parseFigureSpec (CodeBlock (id', cls, attrs) content) + | "pyplot" `elem` cls = Just <$> figureSpec Matplotlib + | "plotly" `elem` cls = Just <$> figureSpec Plotly | otherwise = return Nothing where attrs' = Map.fromList attrs filteredAttrs = filter (\(k, _) -> k `notElem` inclusionKeys) attrs includePath = Map.lookup includePathKey attrs' - figureSpec :: IO FigureSpec - figureSpec = do - includeScript <- fromMaybe (return $ defaultIncludeScript config) $ T.readFile <$> includePath + figureSpec :: RenderingLibrary -> PyplotM FigureSpec + figureSpec lib = do + config <- ask + includeScript <- fromMaybe + (return $ defaultIncludeScript config) + ((liftIO . 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' @@ -75,12 +89,22 @@ withLinks' = fromMaybe (defaultWithLinks config) $ readBool <$> Map.lookup withLinksKey attrs' tightBbox' = isTightBbox config transparent' = isTransparent config - blockAttrs' = (id', filter (/= "pyplot") cls, filteredAttrs) - return $ FigureSpec caption' withLinks' fullScript format dir dpi' tightBbox' transparent' blockAttrs' - -parseFigureSpec _ _ = return Nothing + blockAttrs' = (id', filter (\c -> c `notElem` ["pyplot", "plotly"]) cls, filteredAttrs) + return $ FigureSpec + caption' + withLinks' + fullScript + format + dir + dpi' + lib + tightBbox' + transparent' + blockAttrs' +parseFigureSpec _ = return Nothing + -- | Convert a FigureSpec to a Pandoc block component toImage :: FigureSpec -> Block toImage spec = head . toList $ para $ imageWith attrs' target' "fig:" caption' @@ -91,7 +115,7 @@ attrs' = blockAttrs spec target' = figurePath spec withLinks' = withLinks spec - srcLink = link (replaceExtension target' ".txt") mempty "Source code" + 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, ")"] @@ -100,7 +124,7 @@ -- | Determine the path a figure should have. figurePath :: FigureSpec -> FilePath -figurePath spec = directory spec </> stem spec +figurePath spec = normalise $ directory spec </> stem spec where stem = flip addExtension ext . show . hash ext = extension . saveFormat $ spec @@ -108,12 +132,12 @@ -- | Determine the path to the source code that generated the figure. sourceCodePath :: FigureSpec -> FilePath -sourceCodePath = flip replaceExtension ".txt" . figurePath +sourceCodePath = normalise . flip replaceExtension ".txt" . figurePath -- | The path to the high-resolution figure. hiresFigurePath :: FigureSpec -> FilePath -hiresFigurePath spec = flip replaceExtension (".hires" <> ext) . figurePath $ spec +hiresFigurePath spec = normalise $ flip replaceExtension (".hires" <> ext) . figurePath $ spec where ext = extension . saveFormat $ spec @@ -122,41 +146,62 @@ -- 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 +addPlotCapture spec = mconcat + [ script spec <> "\n" -- Note that the high-resolution figure always has non-transparent background - -- because it is difficult to see the image when opened directly + -- because it is difficult to see the image when opened directly -- in Chrome, for example. - , plotCapture (figurePath spec) (dpi spec) (transparent spec) - , plotCapture (hiresFigurePath spec) (minimum [200, 2 * dpi spec]) False + , plotCapture (renderingLib spec) (figurePath spec) (dpi spec) (transparent spec) (tight') + , plotCapture (renderingLib spec) (hiresFigurePath spec) (minimum [200, 2 * dpi spec]) False (tight') ] where - tight' = tightBbox spec - plotCapture fname' dpi' transparent' = mconcat $ - [ "\nplt.savefig(" - , T.pack $ show fname' -- show is required for quotes - , ", dpi=" - , T.pack $ show dpi' - , ", transparent=" - , T.pack $ show transparent' - , if tight' - then ", bbox_inches=\"tight\")" - else ")" - ] + tight' = if tightBbox spec then ("'tight'" :: T.Text) else ("None" :: T.Text) + -- Note that, especially for Windows, raw strings (r"...") must be used because path separators might + -- be interpreted as escape characters + plotCapture Matplotlib = captureMatplotlib + plotCapture Plotly = capturePlotly +type Tight = T.Text +type IsTransparent = Bool +type RenderingFunc = (FilePath -> Int -> IsTransparent -> Tight -> PythonScript) + + +-- | Capture plot from Matplotlib +-- Note that, especially for Windows, raw strings (r"...") must be used because path separators might +-- be interpreted as escape characters +captureMatplotlib :: RenderingFunc +captureMatplotlib fname' dpi' transparent' tight' = [st| +import matplotlib.pyplot as plt +plt.savefig(r"#{fname'}", dpi=#{dpi'}, transparent=#{transparent''}, bbox_inches=#{tight'}) +|] + where + transparent'' :: T.Text + transparent'' = if transparent' then "True" else "False" + +-- | Capture Plotly figure +-- +-- We are trying to emulate the behavior of "matplotlib.pyplot.savefig" which +-- knows the "current figure". This saves us from contraining users to always +-- have the same Plotly figure name, e.g. "fig" in all examples +capturePlotly :: RenderingFunc +capturePlotly fname' _ _ _ = [st| +import plotly.graph_objects as go +__current_plotly_figure = next(obj for obj in globals().values() if type(obj) == go.Figure) +__current_plotly_figure.write_image("#{fname'}") +|] + + -- | Reader options for captions. readerOptions :: ReaderOptions -readerOptions = def - {readerExtensions = - extensionsFromList +readerOptions = def + {readerExtensions = + extensionsFromList [ Ext_tex_math_dollars - , Ext_superscript + , Ext_superscript , Ext_subscript , Ext_raw_tex - ] + ] } @@ -169,14 +214,14 @@ extractFromBlocks (Pandoc _ blocks) = mconcat $ extractInlines <$> blocks - extractInlines (Plain inlines) = inlines - extractInlines (Para inlines) = inlines + extractInlines (Plain inlines) = inlines + extractInlines (Para inlines) = inlines extractInlines (LineBlock multiinlines) = join multiinlines - extractInlines _ = [] + extractInlines _ = [] -- | Flexible boolean parsing readBool :: String -> Bool readBool s | s `elem` ["True", "true", "'True'", "'true'", "1"] = True | s `elem` ["False", "false", "'False'", "'false'", "0"] = False - | otherwise = error $ mconcat ["Could not parse '", s, "' into a boolean. Please use 'True' or 'False'"] + | otherwise = error $ mconcat ["Could not parse '", s, "' into a boolean. Please use 'True' or 'False'"]
src/Text/Pandoc/Filter/Pyplot/Internal.hs view
@@ -1,6 +1,6 @@ {-| -Module : Text.Pandoc.Filter.Internal +Module : $header$ Copyright : (c) Laurent P René de Cotret, 2019 License : MIT Maintainer : laurent.decotret@outlook.com @@ -17,7 +17,7 @@ , module Text.Pandoc.Filter.Pyplot.Types ) where -import Text.Pandoc.Filter.Pyplot.Configuration -import Text.Pandoc.Filter.Pyplot.FigureSpec -import Text.Pandoc.Filter.Pyplot.Scripting -import Text.Pandoc.Filter.Pyplot.Types+import Text.Pandoc.Filter.Pyplot.Configuration +import Text.Pandoc.Filter.Pyplot.FigureSpec +import Text.Pandoc.Filter.Pyplot.Scripting +import Text.Pandoc.Filter.Pyplot.Types
src/Text/Pandoc/Filter/Pyplot/Scripting.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE OverloadedStrings #-} {-| -Module : Text.Pandoc.Filter.Pyplot.Scripting +Module : $header$ Copyright : (c) Laurent P René de Cotret, 2019 License : MIT Maintainer : laurent.decotret@outlook.com @@ -16,58 +16,63 @@ , runScriptIfNecessary ) where -import Data.Hashable (hash) -import Data.List (intersperse) -import Data.Monoid (Any(..), (<>)) -import qualified Data.Text as T -import qualified Data.Text.IO as T +import Control.Monad.IO.Class +import Control.Monad.Reader.Class -import System.Directory (createDirectoryIfMissing, - doesFileExist) -import System.Exit (ExitCode (..)) -import System.FilePath ((</>), takeDirectory) -import System.IO.Temp (getCanonicalTemporaryDirectory) -import System.Process.Typed (runProcess, shell) +import Data.Hashable (hash) +import Data.List (intersperse) +import Data.Monoid (Any (..), (<>)) +import qualified Data.Text as T +import qualified Data.Text.IO as T -import Text.Pandoc.Filter.Pyplot.Types +import System.Directory (createDirectoryIfMissing, + doesFileExist) +import System.Exit (ExitCode (..)) +import System.FilePath (takeDirectory, (</>)) +import System.IO.Temp (getCanonicalTemporaryDirectory) +import System.Process.Typed (runProcess, shell) + import Text.Pandoc.Filter.Pyplot.FigureSpec +import Text.Pandoc.Filter.Pyplot.Types -- | Detect the presence of a blocking show call, for example "plt.show()" checkBlockingShowCall :: PythonScript -> CheckResult -checkBlockingShowCall script' = - if hasShowCall +checkBlockingShowCall script' = + if hasShowCall then CheckFailed "The script has a blocking call to `matplotlib.pyplot.show`. " - else CheckPassed + else CheckPassed where scriptLines = T.lines script' hasShowCall = getAny $ mconcat $ Any <$> [ "plt.show()" `elem` scriptLines , "pyplot.show()" `elem` scriptLines , "matplotlib.pyplot.show()" `elem` scriptLines + , "fig.show()" `elem` scriptLines ] + -- | List of all script checks -- This might be overkill right now but extension to other languages will be easier scriptChecks :: [PythonScript -> CheckResult] scriptChecks = [checkBlockingShowCall] + -- | Take a python script in string form, write it in a temporary directory, -- then execute it. -runTempPythonScript :: String -- ^ Interpreter (e.g. "python" or "python35") - -> [String] -- ^ Command-line flags - -> PythonScript -- ^ Content of the script - -> IO ScriptResult -- ^ Result. -runTempPythonScript interpreter' flags' script' = case checkResult of +runTempPythonScript :: PythonScript -- ^ Content of the script + -> PyplotM ScriptResult -- ^ Result. +runTempPythonScript script' = case checkResult of CheckFailed msg -> return $ ScriptChecksFailed msg CheckPassed -> do -- We involve the script hash as a temporary filename -- so that there is never any collision - scriptPath <- (</> hashedPath) <$> getCanonicalTemporaryDirectory - T.writeFile scriptPath script' - + scriptPath <- liftIO $ (</> hashedPath) <$> getCanonicalTemporaryDirectory + liftIO $ T.writeFile scriptPath script' + interpreter' <- asks interpreter + flags' <- asks flags let command = mconcat . intersperse " " $ [interpreter'] <> flags' <> [show scriptPath] - ec <- runProcess . shell $ command + ec <- liftIO $ runProcess . shell $ command case ec of ExitSuccess -> return ScriptSuccess ExitFailure code -> return $ ScriptFailure code @@ -75,21 +80,25 @@ checkResult = mconcat $ scriptChecks <*> [script'] hashedPath = show . hash $ script' + -- | Run the Python script. In case the file already exists, we can safely assume -- there is no need to re-run it. -runScriptIfNecessary :: Configuration -> FigureSpec -> IO ScriptResult -runScriptIfNecessary config spec = do - createDirectoryIfMissing True . takeDirectory $ figurePath spec +runScriptIfNecessary :: FigureSpec + -> PyplotM ScriptResult +runScriptIfNecessary spec = do + liftIO $ createDirectoryIfMissing True . takeDirectory $ figurePath spec - fileAlreadyExists <- doesFileExist $ figurePath spec + fileAlreadyExists <- liftIO . doesFileExist $ figurePath spec result <- if fileAlreadyExists then return ScriptSuccess - else runTempPythonScript (interpreter config) (flags config) scriptWithCapture - + else runTempPythonScript scriptWithCapture + case result of - ScriptSuccess -> T.writeFile (sourceCodePath spec) (script spec) >> return ScriptSuccess + ScriptSuccess -> liftIO $ T.writeFile (sourceCodePath spec) (script spec) >> return ScriptSuccess ScriptFailure code -> return $ ScriptFailure code ScriptChecksFailed msg -> return $ ScriptChecksFailed msg where - scriptWithCapture = addPlotCapture spec+ scriptWithCapture = addPlotCapture spec + +
src/Text/Pandoc/Filter/Pyplot/Types.hs view
@@ -1,8 +1,8 @@ {-# LANGUAGE CPP #-} -{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} {-| -Module : Text.Pandoc.Filter.Pyplot.Types +Module : $header$ Copyright : (c) Laurent P René de Cotret, 2019 License : MIT Maintainer : laurent.decotret@outlook.com @@ -14,18 +14,20 @@ module Text.Pandoc.Filter.Pyplot.Types where -import Data.Char (toLower) -import Data.Default.Class (Default, def) -import Data.Hashable (Hashable) -import Data.Semigroup as Sem -import Data.Text (Text, pack) -import Data.Yaml +import Control.Monad.Reader -import GHC.Generics (Generic) +import Data.Char (toLower) +import Data.Default.Class (Default, def) +import Data.Hashable (Hashable) +import Data.Semigroup as Sem +import Data.Text (Text, pack) +import Data.Yaml -import Text.Pandoc.Definition (Attr) +import GHC.Generics (Generic) +import Text.Pandoc.Definition (Attr) + -- | Keys that pandoc-pyplot will look for in code blocks. These are only exported for testing purposes. directoryKey, captionKey, dpiKey, includePathKey, saveFormatKey, withLinksKey, isTightBboxKey, isTransparentKey :: String directoryKey = "directory" @@ -37,7 +39,8 @@ isTightBboxKey = "tight_bbox" isTransparentKey = "transparent" --- | list of all keys related to pandoc-pyplot that + +-- | list of all keys related to pandoc-pyplot that -- can be specified in source material. inclusionKeys :: [String] inclusionKeys = [ directoryKey @@ -51,10 +54,25 @@ ] +-- | Monad in which to run pandoc-pyplot computations +type PyplotM a = ReaderT Configuration IO a + + -- | String representation of a Python script type PythonScript = Text +-- | Rendering library +-- +-- @since 2.2.0.0 +data RenderingLibrary + = Matplotlib -- ^ Rendering via the Matplotlib library. This library has the most features. + | Plotly -- ^ Rendering via the Plotly library. + deriving (Show, Eq, Generic) + +instance Hashable RenderingLibrary + + -- | Possible result of running a Python script data ScriptResult = ScriptSuccess @@ -63,19 +81,19 @@ -- | Result of checking scripts for problems -data CheckResult +data CheckResult = CheckPassed | CheckFailed String deriving (Eq) instance Sem.Semigroup CheckResult where - (<>) CheckPassed a = a - (<>) a CheckPassed = a + (<>) CheckPassed a = a + (<>) a CheckPassed = a (<>) (CheckFailed msg1) (CheckFailed msg2) = CheckFailed (msg1 <> msg2) instance Monoid CheckResult where mempty = CheckPassed - + #if !(MIN_VERSION_base(4,11,0)) mappend = (<>) #endif @@ -86,13 +104,15 @@ = ScriptError Int -- ^ Running Python script has yielded an error | ScriptChecksFailedError String -- ^ Python script did not pass all checks deriving (Eq) - + instance Show PandocPyplotError where show (ScriptError exitcode) = "Script error: plot could not be generated. Exit code " <> (show exitcode) show (ScriptChecksFailedError msg) = "Script did not pass all checks: " <> msg --- | Generated figure file format supported by pandoc-pyplot. +-- | Generated figure file format supported by pandoc-pyplot. +-- Note: all formats are supported by Matplotlib, but not all +-- formats are supported by Plotly data SaveFormat = PNG | PDF @@ -130,7 +150,7 @@ extension :: SaveFormat -> String extension fmt = mconcat [".", fmap toLower . show $ fmt] --- | Default interpreter should be Python 3, which has a different +-- | Default interpreter should be Python 3, which has a different -- name on Windows ("python") vs Unix ("python3") -- -- @since 2.1.2.0 @@ -142,21 +162,21 @@ #endif -- | Configuration of pandoc-pyplot, describing the default behavior --- of the filter. +-- 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 +data Configuration + = Configuration { defaultDirectory :: FilePath -- ^ The default directory where figures will be saved. , defaultIncludeScript :: PythonScript -- ^ The default script to run before other instructions. , defaultWithLinks :: Bool -- ^ The default behavior of whether or not to include links to source code and high-res , defaultSaveFormat :: SaveFormat -- ^ The default save format of generated figures. - , defaultDPI :: Int -- ^ The default dots-per-inch value for generated figures. - , isTightBbox :: Bool -- ^ Whether the figures should be saved with @bbox_inches="tight"@ or not. Useful for larger figures with subplots. - , isTransparent :: Bool -- ^ If True, figures will be saved with transparent background rather than solid color. + , defaultDPI :: Int -- ^ The default dots-per-inch value for generated figures. Matplotlib only, ignored otherwise. + , isTightBbox :: Bool -- ^ Whether the figures should be saved with @bbox_inches="tight"@ or not. Useful for larger figures with subplots. Matplotlib only, ignored otherwise. + , isTransparent :: Bool -- ^ If True, figures will be saved with transparent background rather than solid color. .Matplotlib only, ignored otherwise. , interpreter :: String -- ^ The name of the interpreter to use to render figures. , flags :: [String] -- ^ Command-line flags to be passed to the Python interpreger, e.g. ["-O", "-Wignore"] } @@ -176,35 +196,36 @@ } instance ToJSON Configuration where - toJSON (Configuration dir' _ withLinks' savefmt' dpi' tightbbox' transparent' interp' flags') = + toJSON (Configuration dir' _ withLinks' savefmt' dpi' tightbbox' transparent' interp' flags') = -- We ignore the include script as we want to examplify that -- this is for a filepath - object [ pack directoryKey .= dir' + object [ pack directoryKey .= dir' , pack includePathKey .= ("example.py" :: FilePath) , pack withLinksKey .= withLinks' , pack dpiKey .= dpi' , pack saveFormatKey .= (toLower <$> show savefmt') , pack isTightBboxKey .= tightbbox' , pack isTransparentKey .= transparent' - , "interpreter" .= interp' - , "flags" .= flags' + , "interpreter" .= interp' + , "flags" .= flags' ] - --- | Datatype containing all parameters required to run pandoc-pyplot. + +-- | Datatype containing all parameters required to run pandoc-pyplot. -- -- It is assumed that once a @FigureSpec@ has been created, no configuration -- can overload it; hence, a @FigureSpec@ completely encodes a particular figure. data FigureSpec = FigureSpec - { caption :: String -- ^ Figure caption. - , withLinks :: Bool -- ^ Append links to source code and high-dpi figure to 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. - , tightBbox :: Bool -- ^ Enforce tight bounding-box with @bbox_inches="tight"@. - , transparent :: Bool -- ^ Make figure background transparent. - , blockAttrs :: Attr -- ^ Attributes not related to @pandoc-pyplot@ will be propagated. + { caption :: String -- ^ Figure caption. + , withLinks :: Bool -- ^ Append links to source code and high-dpi figure to 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. This option only affects the Matplotlib backend. + , renderingLib :: RenderingLibrary -- ^ Rendering library. + , tightBbox :: Bool -- ^ Enforce tight bounding-box with @bbox_inches="tight"@. This option only affects the Matplotlib backend. + , transparent :: Bool -- ^ Make figure background transparent. This option only affects the Matplotlib backend. + , blockAttrs :: Attr -- ^ Attributes not related to @pandoc-pyplot@ will be propagated. } deriving Generic instance Hashable FigureSpec -- From Generic
stack.yaml view
@@ -17,7 +17,7 @@ # # resolver: ./custom-snapshot.yaml # resolver: https://example.com/snapshots/2018-01-01.yaml -resolver: lts-13.16 +resolver: lts-14.1 # User packages to be built. # Various formats can be used as shown in the example below.
test/Main.hs view
@@ -3,12 +3,13 @@ module Main where -import Control.Monad (unless) +import Control.Monad (unless) +import Control.Monad.Reader -import Data.Default.Class (def) -import Data.List (isInfixOf, isSuffixOf) -import Data.Monoid ((<>)) -import Data.Text (unpack) +import Data.Default.Class (def) +import Data.List (isInfixOf, isSuffixOf) +import Data.Monoid ((<>)) +import Data.Text (unpack) import Test.Tasty import Test.Tasty.HUnit @@ -16,18 +17,19 @@ import Text.Pandoc.Filter.Pyplot import Text.Pandoc.Filter.Pyplot.Internal +import qualified Text.Pandoc.Builder as B +import qualified Text.Pandoc.Definition as B import Text.Pandoc.JSON -import qualified Text.Pandoc.Builder as B -import qualified Text.Pandoc.Definition as B -import System.Directory (createDirectory, - createDirectoryIfMissing, - doesDirectoryExist, - doesFileExist, listDirectory, - removeDirectoryRecursive, - removePathForcibly) -import System.FilePath ((</>), takeExtensions) -import System.IO.Temp (getCanonicalTemporaryDirectory) +import System.Directory (createDirectory, + createDirectoryIfMissing, + doesDirectoryExist, + doesFileExist, + listDirectory, + removeDirectoryRecursive, + removePathForcibly) +import System.FilePath (takeExtensions, (</>)) +import System.IO.Temp (getCanonicalTemporaryDirectory) main :: IO () main = @@ -87,8 +89,8 @@ 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 @@ -114,7 +116,7 @@ ensureDirectoryExistsAndEmpty tempDir let codeBlock = (addDirectory tempDir $ plotCodeBlock "import matplotlib.pyplot as plt\n") - _ <- makePlot' def codeBlock + _ <- runReaderT (makePlot' codeBlock) def filesCreated <- length <$> listDirectory tempDir assertEqual "" filesCreated 3 @@ -129,7 +131,7 @@ let codeBlock = (addInclusion "test/fixtures/include.py" $ addDirectory tempDir $ plotCodeBlock "import matplotlib.pyplot as plt\n") - _ <- makePlot' def codeBlock + _ <- runReaderT (makePlot' codeBlock) def inclusion <- readFile "test/fixtures/include.py" sourcePath <- head . filter (isExtensionOf "txt") <$> listDirectory tempDir src <- readFile (tempDir </> sourcePath) @@ -148,7 +150,7 @@ addDirectory tempDir $ plotCodeBlock "import matplotlib.pyplot as plt\nplt.figure()\nplt.plot([1,2], [1,2])") - _ <- makePlot' def codeBlock + _ <- runReaderT (makePlot' codeBlock) def numberjpgFiles <- length <$> filter (isExtensionOf (extension JPG)) <$> listDirectory tempDir @@ -164,7 +166,7 @@ ensureDirectoryExistsAndEmpty tempDir let codeBlock = addDirectory tempDir $ plotCodeBlock "import matplotlib.pyplot as plt\nplt.show()" - result <- makePlot' def codeBlock + result <- runReaderT (makePlot' codeBlock) def case result of Right block -> assertFailure "did not catch the expected blocking call" Left error -> @@ -173,7 +175,7 @@ else assertFailure "An error was caught but not the expected blocking call" where isCheckError (ScriptChecksFailedError msg) = True - isCheckError _ = False + isCheckError _ = False ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- @@ -188,16 +190,16 @@ -- constructed let expected = [B.Strong [B.Str "caption"]] codeBlock = addDirectory tempDir $ addCaption "**caption**" $ plotCodeBlock "import matplotlib.pyplot as plt" - result <- makePlot' def codeBlock + result <- runReaderT (makePlot' codeBlock) def case result of - Left error -> assertFailure $ "an error occured: " <> show error + Left error -> assertFailure $ "an error occured: " <> show error Right block -> assertIsInfix expected (extractCaption block) where extractCaption (B.Para blocks) = extractImageCaption . head $ blocks - extractCaption _ = mempty + extractCaption _ = mempty extractImageCaption (Image _ c _) = c - extractImageCaption _ = mempty + extractImageCaption _ = mempty ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- @@ -212,16 +214,16 @@ -- constructed let expected = mempty codeBlock = addWithLinks False $ addDirectory tempDir $ addCaption mempty $ plotCodeBlock "import matplotlib.pyplot as plt" - result <- makePlot' def codeBlock + result <- runReaderT (makePlot' codeBlock) def case result of - Left error -> assertFailure $ "an error occured: " <> show error + Left error -> assertFailure $ "an error occured: " <> show error Right block -> assertIsInfix expected (extractCaption block) where extractCaption (B.Para blocks) = extractImageCaption . head $ blocks - extractCaption _ = mempty + extractCaption _ = mempty extractImageCaption (Image _ c _) = c - extractImageCaption _ = mempty + extractImageCaption _ = mempty ------------------------------------------------------------------------------- @@ -241,10 +243,10 @@ -- The default from config says the save format should be JPG -- but the code block save format="png" - let codeBlock = (addSaveFormat PNG $ - plotCodeBlock + let codeBlock = (addSaveFormat PNG $ + plotCodeBlock "import matplotlib.pyplot as plt\nplt.figure()\nplt.plot([1,2], [1,2])") - _ <- makePlot' config codeBlock + _ <- runReaderT (makePlot' codeBlock) config numberjpgFiles <- length <$> filter (isExtensionOf (extension JPG)) <$> @@ -264,7 +266,7 @@ config <- testConfig let codeBlock = plotCodeBlock "import matplotlib.pyplot as plt\nplt.figure()\nplt.plot([1,2], [1,2])" - _ <- makePlot' config codeBlock + _ <- runReaderT (makePlot' codeBlock) config numberjpgFiles <- length <$> filter (isExtensionOf (extension JPG)) <$> @@ -273,7 +275,7 @@ ------------------------------------------------------------------------------- testBuildConfiguration :: TestTree -testBuildConfiguration = +testBuildConfiguration = testCase "configuration is correctly parsed" $ do let config = def { defaultDirectory = "generated/other" , defaultSaveFormat = JPG @@ -283,4 +285,4 @@ , isTransparent = True } parsedConfig <- configuration "test/fixtures/.pandoc-pyplot.yml" - assertEqual "" config parsedConfig+ assertEqual "" config parsedConfig