summaryrefslogtreecommitdiff
path: root/src/Text/Pandoc/Filter
diff options
context:
space:
mode:
authorLaurentRDC <>2019-04-07 01:10:00 (GMT)
committerhdiff <hdiff@hdiff.luite.com>2019-04-07 01:10:00 (GMT)
commit30fea466d0dd6beb5e55a851e802ea7b470a35d0 (patch)
treeb57fc92c54441a600f9db9f8760e15c72aad4158 /src/Text/Pandoc/Filter
parentb870c74b0ab480ca62602ebfd5fe89e3fc4f7094 (diff)
version 2.1.0.02.1.0.0
Diffstat (limited to 'src/Text/Pandoc/Filter')
-rw-r--r--src/Text/Pandoc/Filter/Pyplot.hs86
-rw-r--r--src/Text/Pandoc/Filter/Pyplot/Configuration.hs126
-rw-r--r--src/Text/Pandoc/Filter/Pyplot/FigureSpec.hs (renamed from src/Text/Pandoc/Filter/FigureSpec.hs)59
-rw-r--r--src/Text/Pandoc/Filter/Pyplot/Internal.hs21
-rw-r--r--src/Text/Pandoc/Filter/Pyplot/Scripting.hs (renamed from src/Text/Pandoc/Filter/Scripting.hs)29
5 files changed, 241 insertions, 80 deletions
diff --git a/src/Text/Pandoc/Filter/Pyplot.hs b/src/Text/Pandoc/Filter/Pyplot.hs
index 7c545d9..6df85ae 100644
--- a/src/Text/Pandoc/Filter/Pyplot.hs
+++ b/src/Text/Pandoc/Filter/Pyplot.hs
@@ -1,6 +1,5 @@
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE Unsafe #-}
{-|
Module : Text.Pandoc.Filter.Pyplot
@@ -66,23 +65,26 @@ makePlotPandocCompiler =
@
-}
-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,11 +101,8 @@ import System.FilePath (makeValid, takeDirectory)
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
@@ -115,52 +114,40 @@ instance Show PandocPyplotError where
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 @@ runScriptIfNecessary spec = do
-- | 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 @@ makePlot' block = do
-- 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
diff --git a/src/Text/Pandoc/Filter/Pyplot/Configuration.hs b/src/Text/Pandoc/Filter/Pyplot/Configuration.hs
new file mode 100644
index 0000000..443afc0
--- /dev/null
+++ b/src/Text/Pandoc/Filter/Pyplot/Configuration.hs
@@ -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 \ No newline at end of file
diff --git a/src/Text/Pandoc/Filter/FigureSpec.hs b/src/Text/Pandoc/Filter/Pyplot/FigureSpec.hs
index ae48bbd..d8480ab 100644
--- a/src/Text/Pandoc/Filter/FigureSpec.hs
+++ b/src/Text/Pandoc/Filter/Pyplot/FigureSpec.hs
@@ -1,7 +1,7 @@
{-# LANGUAGE OverloadedStrings #-}
{-|
-Module : Text.Pandoc.Filter.FigureSpec
+Module : Text.Pandoc.Filter.Pyplot.FigureSpec
Copyright : (c) Laurent P René de Cotret, 2019
License : MIT
Maintainer : laurent.decotret@outlook.com
@@ -11,7 +11,7 @@ Portability : portable
This module defines types and functions that help
with keeping track of figure specifications
-}
-module Text.Pandoc.Filter.FigureSpec
+module Text.Pandoc.Filter.Pyplot.FigureSpec
( FigureSpec(..)
, SaveFormat(..)
, saveFormatFromString
@@ -25,8 +25,10 @@ module Text.Pandoc.Filter.FigureSpec
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,
@@ -34,13 +36,14 @@ import System.FilePath (FilePath, addExtension,
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)
+import Text.Pandoc.Filter.Pyplot.Scripting
+
readerOptions :: ReaderOptions
readerOptions = def
{readerExtensions =
@@ -51,7 +54,8 @@ readerOptions = def
]
}
--- | Read a figure caption in Markdown format.
+-- | 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
@@ -64,45 +68,60 @@ captionReader t = either (const Nothing) (Just . extractFromBlocks) $ runPure $
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` ["jpg", "jpeg", "JPG", "JPEG", ".jpg", ".jpeg"] = Just JPG
| 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 PNG = ".png"
-extension PDF = ".pdf"
-extension SVG = ".svg"
-extension JPG = ".jpg"
-extension EPS = ".eps"
-
--- | Datatype containing all parameters required
--- to run pandoc-pyplot
+extension fmt = mconcat [".", fmap toLower . show $ fmt]
+
+-- | Datatype containing all parameters required to run pandoc-pyplot
data FigureSpec = FigureSpec
- { caption :: String -- ^ Figure caption.
+ { 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.
+ , 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)
+ 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
@@ -124,7 +143,7 @@ figurePath :: FigureSpec -> FilePath
figurePath spec = (directory spec </> stem spec)
where
stem = flip addExtension ext . show . hash
- ext = extension . saveFormat $ spec
+ ext = extension . saveFormat $ spec
-- | Determine the path to the source code that generated the figure.
sourceCodePath :: FigureSpec -> FilePath
diff --git a/src/Text/Pandoc/Filter/Pyplot/Internal.hs b/src/Text/Pandoc/Filter/Pyplot/Internal.hs
new file mode 100644
index 0000000..5c8fee5
--- /dev/null
+++ b/src/Text/Pandoc/Filter/Pyplot/Internal.hs
@@ -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 \ No newline at end of file
diff --git a/src/Text/Pandoc/Filter/Scripting.hs b/src/Text/Pandoc/Filter/Pyplot/Scripting.hs
index 889db6d..ec90780 100644
--- a/src/Text/Pandoc/Filter/Scripting.hs
+++ b/src/Text/Pandoc/Filter/Pyplot/Scripting.hs
@@ -1,8 +1,7 @@
{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE Unsafe #-}
{-|
-Module : Text.Pandoc.Filter.Scripting
+Module : Text.Pandoc.Filter.Pyplot.Scripting
Copyright : (c) Laurent P René de Cotret, 2019
License : MIT
Maintainer : laurent.decotret@outlook.com
@@ -12,26 +11,24 @@ Portability : portable
This module defines types and functions that help
with running Python scripts.
-}
-module Text.Pandoc.Filter.Scripting
+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 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
@@ -42,20 +39,18 @@ data ScriptResult
-- | 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
+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
- = do
scriptPath <- (</> hashedPath) <$> getCanonicalTemporaryDirectory
T.writeFile scriptPath script
- -- Execute script
- ec <- runProcess $ shell $ "python " <> (show scriptPath)
+
+ ec <- runProcess $ shell $ mconcat [interpreter, " ", show scriptPath]
case ec of
- ExitSuccess -> return ScriptSuccess
+ ExitSuccess -> return ScriptSuccess
ExitFailure code -> return $ ScriptFailure code
where
hashedPath = show . hash $ script
@@ -70,4 +65,4 @@ hasBlockingShowCall script =
]
where
scriptLines = T.lines script
- anyOf xs = getAny $ mconcat $ Any <$> xs
+ anyOf xs = getAny $ mconcat $ Any <$> xs \ No newline at end of file