summaryrefslogtreecommitdiff
path: root/src/Text/Pandoc/Filter
diff options
context:
space:
mode:
authorLaurentRDC <>2019-11-05 13:36:00 (GMT)
committerhdiff <hdiff@hdiff.luite.com>2019-11-05 13:36:00 (GMT)
commit0896c8b21460ee840bb016aa81192720dfcc1eb4 (patch)
treed6f08c8cf271d24eb3c3f775db6282561cbfc124 /src/Text/Pandoc/Filter
parent2ae5738bae724386a9710042cfc7b0e4939dcfc1 (diff)
version 2.2.0.02.2.0.0
Diffstat (limited to 'src/Text/Pandoc/Filter')
-rw-r--r--src/Text/Pandoc/Filter/Pyplot.hs48
-rw-r--r--src/Text/Pandoc/Filter/Pyplot/Configuration.hs73
-rw-r--r--src/Text/Pandoc/Filter/Pyplot/FigureSpec.hs175
-rw-r--r--src/Text/Pandoc/Filter/Pyplot/Internal.hs10
-rw-r--r--src/Text/Pandoc/Filter/Pyplot/Scripting.hs75
-rw-r--r--src/Text/Pandoc/Filter/Pyplot/Types.hs101
6 files changed, 281 insertions, 201 deletions
diff --git a/src/Text/Pandoc/Filter/Pyplot.hs b/src/Text/Pandoc/Filter/Pyplot.hs
index e74bb9c..37302bd 100644
--- a/src/Text/Pandoc/Filter/Pyplot.hs
+++ b/src/Text/Pandoc/Filter/Pyplot.hs
@@ -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 @@ module Text.Pandoc.Filter.Pyplot (
, 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 @@ makePlot = makePlotWithConfig def
--
-- @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.
diff --git a/src/Text/Pandoc/Filter/Pyplot/Configuration.hs b/src/Text/Pandoc/Filter/Pyplot/Configuration.hs
index ae2af6b..c2d42c7 100644
--- a/src/Text/Pandoc/Filter/Pyplot/Configuration.hs
+++ b/src/Text/Pandoc/Filter/Pyplot/Configuration.hs
@@ -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 @@ module Text.Pandoc.Filter.Pyplot.Configuration (
, 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 @@ data ConfigPrecursor
, 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 @@ writeConfig fp config = do
fileExists <- doesFileExist fp
if fileExists
then error $ mconcat ["File ", fp, " already exists."]
- else encodeFile fp config \ No newline at end of file
+ else encodeFile fp config
diff --git a/src/Text/Pandoc/Filter/Pyplot/FigureSpec.hs b/src/Text/Pandoc/Filter/Pyplot/FigureSpec.hs
index bfbec4f..0c812b8 100644
--- a/src/Text/Pandoc/Filter/Pyplot/FigureSpec.hs
+++ b/src/Text/Pandoc/Filter/Pyplot/FigureSpec.hs
@@ -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 @@ module Text.Pandoc.Filter.Pyplot.FigureSpec
, 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,10 +89,20 @@ parseFigureSpec config (CodeBlock (id', cls, attrs) content)
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
@@ -91,7 +115,7 @@ toImage spec = head . toList $ para $ imageWith attrs' target' "fig:" caption'
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 @@ toImage spec = head . toList $ para $ imageWith attrs' target' "fig:" caption'
-- | 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 @@ figurePath spec = directory spec </> stem spec
-- | 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 @@ hiresFigurePath spec = flip replaceExtension (".hires" <> ext) . figurePath $ sp
-- 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 @@ captionReader t = either (const Nothing) (Just . extractFromBlocks) $ runPure $
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'"] \ No newline at end of file
+ | otherwise = error $ mconcat ["Could not parse '", s, "' into a boolean. Please use 'True' or 'False'"]
diff --git a/src/Text/Pandoc/Filter/Pyplot/Internal.hs b/src/Text/Pandoc/Filter/Pyplot/Internal.hs
index a3970c7..f61d2a4 100644
--- a/src/Text/Pandoc/Filter/Pyplot/Internal.hs
+++ b/src/Text/Pandoc/Filter/Pyplot/Internal.hs
@@ -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.Internal (
, 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 \ No newline at end of file
+import Text.Pandoc.Filter.Pyplot.Configuration
+import Text.Pandoc.Filter.Pyplot.FigureSpec
+import Text.Pandoc.Filter.Pyplot.Scripting
+import Text.Pandoc.Filter.Pyplot.Types
diff --git a/src/Text/Pandoc/Filter/Pyplot/Scripting.hs b/src/Text/Pandoc/Filter/Pyplot/Scripting.hs
index 0ed4642..15d251c 100644
--- a/src/Text/Pandoc/Filter/Pyplot/Scripting.hs
+++ b/src/Text/Pandoc/Filter/Pyplot/Scripting.hs
@@ -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 @@ module Text.Pandoc.Filter.Pyplot.Scripting
, 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 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.Types
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 @@ runTempPythonScript interpreter' flags' script' = case checkResult of
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 \ No newline at end of file
+ scriptWithCapture = addPlotCapture spec
+
+
diff --git a/src/Text/Pandoc/Filter/Pyplot/Types.hs b/src/Text/Pandoc/Filter/Pyplot/Types.hs
index 408cbde..fd3eae0 100644
--- a/src/Text/Pandoc/Filter/Pyplot/Types.hs
+++ b/src/Text/Pandoc/Filter/Pyplot/Types.hs
@@ -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,16 +14,18 @@ This module defines types in use in pandoc-pyplot
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.
@@ -37,7 +39,8 @@ withLinksKey = "links"
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 @@ inclusionKeys = [ directoryKey
]
+-- | 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 @@ data ScriptResult
-- | 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 @@ data PandocPyplotError
= 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 @@ saveFormatFromString s
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 @@ defaultPlatformInterpreter = "python3"
#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 Default Configuration where
}
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