pandoc-pyplot (empty) → 1.0.0.0
raw patch · 10 files changed
+556/−0 lines, 10 filesdep +basedep +containersdep +directorysetup-changed
Dependencies added: base, containers, directory, filepath, pandoc-pyplot, pandoc-types, temporary, typed-process
Files
- CHANGELOG.md +7/−0
- LICENSE.md +23/−0
- README.md +133/−0
- Setup.hs +7/−0
- executable/Main.hs +41/−0
- package.yaml +50/−0
- pandoc-pyplot.cabal +62/−0
- src/Text/Pandoc/Filter/Pyplot.hs +111/−0
- src/Text/Pandoc/Filter/Scripting.hs +58/−0
- stack.yaml +64/−0
+ CHANGELOG.md view
@@ -0,0 +1,7 @@+# Change log++pandoc-pyplot uses [Semantic Versioning][].+The change log is available through the [releases on GitHub][].++[Semantic Versioning]: http://semver.org/spec/v2.0.0.html+[releases on GitHub]: https://github.com/githubuser/pandoc-pyplot/releases
+ LICENSE.md view
@@ -0,0 +1,23 @@+[The MIT License (MIT)][]++Copyright (c) 2018 Author name here++Permission is hereby granted, free of charge, to any person obtaining a copy of+this software and associated documentation files (the "Software"), to deal in+the Software without restriction, including without limitation the rights to+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies+of the Software, and to permit persons to whom the Software is furnished to do+so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.++[The MIT License (MIT)]: https://opensource.org/licenses/MIT
+ README.md view
@@ -0,0 +1,133 @@+# pandoc-pyplot + +[](https://ci.appveyor.com/project/LaurentRDC/pandoc-pyplot) + +_A Pandoc filter for generating figures with Matplotlib from code directly in documents_ + +Inspired by [sphinx](https://sphinxdoc.org)'s `plot_directive`, `pandoc-pyplot` helps turn Python code present in your documents to embedded Matplotlib figures. + +## Usage + +The filter recognizes code blocks with the `plot_target` attribute present. It will run the script in the associated code block in a Python interpreter and capture the generated Matplotlib figure. This captured figure will be saved in the located specific by `plot_target`. + +### Basic example + +Here is a basic example using the scripting `matplotlib.pyplot` API: + +```markdown + ```{plot_target=my_figure.jpg} + import matplotlib.pyplot as plt + + plt.figure() + plt.plot([0,1,2,3,4], [1,2,3,4,5]) + plt.title('This is an example figure') + ``` +``` + +`pandoc-pyplot` will determine whether the `plot_target` is a relative or absolute path. In case of a relative path (like above), all paths will be considered relative to the current working directory. + +We can control the format of the output file by changing the `plot_target` file extension. All formats supported by Matplotlib on your machine are available. + +Putting the above in `input.md`, we can then generate the plot and embed it: + +```bash +pandoc --filter pandoc-pyplot input.md --output output.html +``` + +or + +```bash +pandoc --filter pandoc-pyplot input.md output.pdf +``` + +or any other output format you want. There are more examples in the source repository, in the `\examples` directory. + +### Link to source code + +In case of an output format that supports links (e.g. HTML), the embedded image generated by `pandoc-pyplot` will be a link to the source code which was used to generate the file. Therefore, other people can see what Python code was used to create your figures. + +### Alternate text + +You can also specify some alternate text for your image. This is done using the optional `plot_alt` parameter: + +```markdown + ```{plot_target=my_figure.jpg, plot_alt="This is a simple figure"} + import matplotlib.pyplot as plt + + plt.figure() + plt.plot([0,1,2,3,4], [1,2,3,4,5]) + plt.title('This is an example figure') + ``` +``` + +## Requirements + +This filter only works with the Matplotlib plotting library. Therefore, you need [Matplotlib](matplotlib.org) and a Python interpreter. The python interpreter is expected to be discoverable using the name `"python"` (as opposed to `"python3"`, for example) + +## Running the filter + +The filter program must be in your `PATH`. In case it is, you can use the filter with Pandoc as follows: + +```bash +pandoc --filter pandoc-pyplot input.md output.html +``` + +Another example with PDF output: + +```bash +pandoc --filter pandoc-pyplot input.md output.pdf +``` + +Python exceptions will be printed to screen in case of a problem. + +`pandoc-pyplot` has a very limited command-line interface. Take a look at the help available using the `-h` or `--help` argument: + +```bash +pandoc-pyplot --help +``` + +## Usage as a Haskell library + +To include the functionality of `pandoc-pyplot` in a Haskell package, you can use the `makePlot` function: + +```haskell +-- From pandoc-types +import Text.Pandoc.Walk (walkM) +import Text.Pandoc.Definition (Pandoc) +-- From pandoc-pyplot +import Text.Pandoc.Filter.Pyplot (makePlot) + +transformDocument :: Pandoc -> IO Pandoc +transformDocument = walkM makePlot +``` + +## Usage with Hakyll + +This filter was originally designed to be used with [Hakyll](https://jaspervdj.be/hakyll/). In case you want to use the filter with your own Hakyll setup, you must create a transform function first: + +```haskell +-- From pandoc-types +import Text.Pandoc (Pandoc) +import Text.Pandoc.Walk (walkM) + +-- from pandoc-pyplot +import Text.Pandoc.Filter.Pyplot (makePlot) + +import Hakyll + +plotTransform :: Pandoc -> IO Pandoc +plotTransform = walkM . makePlot + +-- Unsafe compiler is required because of the interaction +-- in IO (i.e. running an external Python script). +makePlotPandocCompiler :: Compiler (Item String) +makePlotPandocCompiler = + pandocCompilerWithTransformM + defaultHakyllReaderOptions + defaultHakyllWriterOptions + (unsafeCompiler . plotTransform) +``` + +## Aknowledgements + +This package is inspired from [`pandoc-include-code`](https://github.com/owickstrom/pandoc-include-code).
+ Setup.hs view
@@ -0,0 +1,7 @@+-- This script is used to build and install your package. Typically you don't+-- need to change it. The Cabal documentation has more information about this+-- file: <https://www.haskell.org/cabal/users-guide/installing-packages.html>.+import qualified Distribution.Simple++main :: IO ()+main = Distribution.Simple.defaultMain
+ executable/Main.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE LambdaCase #-}+module Main where++import System.Environment (getArgs)++import Text.Pandoc.JSON (toJSONFilter)+import Text.Pandoc.Filter.Pyplot (makePlot)++import qualified Data.Version as V+import Paths_pandoc_pyplot (version)++-- The formatting is borrowed from Python's argparse library+help :: String+help = "\n\+ \\n\+ \ usage: pandoc-pyplot [-h, --help] [-v, --version] \n\+ \\n\+ \ This pandoc filter generates plots from Python code blocks using Matplotlib. \n\+ \ This allows to keep documentation and figures up-to-date.\n\+ \\n\+ \ Optional arguments:\n\+ \ -h, --help Show this help message and exit\n\+ \ -v, --version Show version number and exit \n\+ \\n\+ \ To use with pandoc: \n\+ \ pandoc --filter pandoc-pyplot input.md --output output.html\n\+ \\n\+ \ More information can be found in the repository README, located at \n\+ \ https://github.com/LaurentRDC/pandoc-pyplot\n"++main :: IO ()+main = do+ getArgs >>=+ \case+ (arg:_) + | arg `elem` ["-h", "--help"] -> showHelp+ | arg `elem` ["-v", "--version"] -> showVersion+ _ -> toJSONFilter makePlot + where+ showHelp = putStrLn help+ showVersion = putStrLn (V.showVersion version)
+ package.yaml view
@@ -0,0 +1,50 @@+name: pandoc-pyplot+version: '1.0.0.0'+github: "LaurentRDC/pandoc-pyplot"+license: MIT+author: "Laurent P. René de Cotret"+maintainer: "Laurent P. René de Cotret"+synopsis: A Pandoc filter for including figures generated from Matplotlib+description: + A pandoc filter for including figures generated from Matplotlib.+ Keep the document and Python code in the same location. Output from Matplotlib+ is captured and included as a figure.+category: Documentation++license-file: LICENSE.md++extra-source-files:+- CHANGELOG.md+- LICENSE.md+- package.yaml+- README.md+- stack.yaml++ghc-options: -Wall -Wcompat++library:+ dependencies:+ - base > 4.8 && < 5+ - directory+ - filepath+ - pandoc-types > 1.12 && < 2+ - temporary+ - typed-process+ - containers+ source-dirs: src+ exposed-modules:+ - Text.Pandoc.Filter.Pyplot+ - Text.Pandoc.Filter.Scripting++executables:+ pandoc-pyplot:+ source-dirs: executable+ main: Main.hs+ dependencies:+ - base > 4.8 && < 5+ - pandoc-pyplot+ - pandoc-types > 1.12 && < 2+ ghc-options:+ - -rtsopts+ - -threaded+ - -with-rtsopts=-N
+ pandoc-pyplot.cabal view
@@ -0,0 +1,62 @@+cabal-version: 1.12 ++-- This file has been generated from package.yaml by hpack version 0.30.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: 057cfdb19c595e350f99205195ae2e446cc16a377c1fd0633b87ad68ae4bdc98++name: pandoc-pyplot+version: 1.0.0.0+synopsis: A Pandoc filter for including figures generated from Matplotlib+description: A pandoc filter for including figures generated from Matplotlib. Keep the document and Python code in the same location. Output from Matplotlib is captured and included as a figure.+category: Documentation+homepage: https://github.com/LaurentRDC/pandoc-pyplot#readme+bug-reports: https://github.com/LaurentRDC/pandoc-pyplot/issues+author: Laurent P. René de Cotret+maintainer: Laurent P. René de Cotret+license: MIT+license-file: LICENSE.md+build-type: Simple+extra-source-files:+ CHANGELOG.md+ LICENSE.md+ package.yaml+ README.md+ stack.yaml++source-repository head+ type: git+ location: https://github.com/LaurentRDC/pandoc-pyplot++library+ exposed-modules:+ Text.Pandoc.Filter.Pyplot+ Text.Pandoc.Filter.Scripting+ other-modules:+ Paths_pandoc_pyplot+ hs-source-dirs:+ src+ ghc-options: -Wall -Wcompat+ build-depends:+ base >4.8 && <5+ , containers+ , directory+ , filepath+ , pandoc-types >1.12 && <2+ , temporary+ , typed-process+ default-language: Haskell2010++executable pandoc-pyplot+ main-is: Main.hs+ other-modules:+ Paths_pandoc_pyplot+ hs-source-dirs:+ executable+ ghc-options: -Wall -Wcompat -rtsopts -threaded -with-rtsopts=-N+ build-depends:+ base >4.8 && <5+ , pandoc-pyplot+ , pandoc-types >1.12 && <2+ default-language: Haskell2010
+ src/Text/Pandoc/Filter/Pyplot.hs view
@@ -0,0 +1,111 @@+{-# LANGUAGE MultiWayIf #-} + +module Text.Pandoc.Filter.Pyplot ( + makePlot + , makePlot' + , PandocPyplotError(..) + , showError + ) where + +import Control.Monad ((>=>)) +import qualified Data.Map.Strict as M +import System.FilePath (replaceExtension, isValid) + +import Text.Pandoc.Definition + +import Text.Pandoc.Filter.Scripting + +data PandocPyplotError = ScriptError Int -- ^ Running Python script has yielded an error + | InvalidTargetError FilePath -- ^ Invalid figure path + | BlockingCallError -- ^ Python script contains a block call to 'show()' + +-- | Datatype containing all parameters required +-- to run pandoc-pyplot +data FigureSpec = FigureSpec + { target :: FilePath -- ^ filepath where generated figure will be saved + , alt :: String -- ^ Alternate text for the figure (optional) + , caption :: String -- ^ Figure caption (optional) + } + +-- Keys that pandoc-pyplot will look for in code blocks +targetKey, altTextKey, captionKey :: String +targetKey = "plot_target" +altTextKey = "plot_alt" +captionKey = "plot_caption" + +-- | Determine inclusion specifications from Block attributes. +-- Note that the target key is required, but all other parameters are optional +parseFigureSpec :: M.Map String String -> Maybe FigureSpec +parseFigureSpec attrs = createInclusion <$> M.lookup targetKey attrs + where + defaultAltText = "Figure generated by pandoc-pyplot" + defaultCaption = mempty + createInclusion fname = FigureSpec + { target = fname + , alt = M.findWithDefault defaultAltText altTextKey attrs + , caption = M.findWithDefault defaultCaption captionKey attrs + } + +-- | Format the script source based on figure spec. +formatScriptSource :: FigureSpec -> PythonScript -> PythonScript +formatScriptSource spec script = mconcat [ "# Source code for " <> target spec + , "\n" + , script + ] + +-- | Main routine to include Matplotlib plots. +-- Code blocks containing the attributes @plot_target@ are considered +-- Python plotting scripts. All other possible blocks are ignored. +-- The source code is also saved in another file, which can be access by +-- clicking the image +makePlot' :: Block -> IO (Either PandocPyplotError Block) +makePlot' cb @ (CodeBlock (id', cls, attrs) scriptSource) = + case parseFigureSpec (M.fromList attrs) of + -- Could not parse - leave code block unchanged + Nothing -> return $ Right cb + -- Could parse : run the script and capture output + Just spec -> do + let figurePath = target spec + + if | not (isValid figurePath) -> return $ Left $ InvalidTargetError figurePath + | hasBlockingShowCall scriptSource -> return $ Left $ BlockingCallError + | otherwise -> do + + script <- addPlotCapture figurePath scriptSource + result <- runTempPythonScript script + + case result of + ScriptFailure code -> return $ Left $ ScriptError code + ScriptSuccess -> do + -- Save the original script into a separate file + -- so it can be inspected + -- Note : using a .txt file allows to view source directly + -- in the browser, in the case of HTML output + let sourcePath = replaceExtension figurePath ".txt" + writeFile sourcePath $ formatScriptSource spec scriptSource + + -- Propagate attributes that are not related to pandoc-pyplot + let inclusionKeys = [ targetKey, altTextKey, captionKey ] + filteredAttrs = filter (\(k,_) -> k `notElem` inclusionKeys) attrs + image = Image (id', cls, filteredAttrs) [Str $ alt spec] (figurePath, "") + srcTarget = (sourcePath, "Click on this figure to see the source code") + + -- TODO: use FigureSpec caption + -- We make the figure be a link to the source code + return $ Right $ Para [ + Link nullAttr [image] srcTarget + ] + +makePlot' x = return $ Right x + +-- | Translate filter error to an error message +showError :: PandocPyplotError -> String +showError (ScriptError exitcode) = "Script error: plot could not be generated. Exit code " <> (show exitcode) +showError (InvalidTargetError fname) = "Target filename " <> fname <> " is not valid." +showError BlockingCallError = "Script contains a blocking call to show, like 'plt.show()'" + +-- | Highest-level function that can be walked over a Pandoc tree. +-- All code blocks that have the 'plot_target' parameter will be considered +-- figures. +makePlot :: Block -> IO Block +makePlot = makePlot' >=> either (fail . showError) return
+ src/Text/Pandoc/Filter/Scripting.hs view
@@ -0,0 +1,58 @@+ +module Text.Pandoc.Filter.Scripting ( + runTempPythonScript + , addPlotCapture + , hasBlockingShowCall + , PythonScript + , ScriptResult(..) +) where + +import System.Directory (getCurrentDirectory) +import System.Exit (ExitCode(..)) +import System.FilePath ((</>), isAbsolute) +import System.IO.Temp (getCanonicalTemporaryDirectory) +import System.Process.Typed (runProcess, shell) + +import Data.Monoid (Any(..)) + +type PythonScript = String + +data ScriptResult = ScriptSuccess + | ScriptFailure Int + +-- | Take a python script in string form, write it in a temporary directory, +-- then execute it. +runTempPythonScript :: PythonScript -- ^ Content of the script + -> IO ScriptResult -- ^ Result with exit code. +runTempPythonScript script = do + -- Write script to temporary directory + scriptPath <- (</> "pandoc-pyplot.py") <$> getCanonicalTemporaryDirectory + writeFile scriptPath script + -- Execute script + ec <- runProcess $ shell $ "python " <> (show scriptPath) + case ec of + ExitSuccess -> return ScriptSuccess + ExitFailure code -> return $ ScriptFailure code + +-- | Modify a Python plotting script to save the figure to a filename. +addPlotCapture :: FilePath -- ^ Path where to save the figure + -> PythonScript -- ^ Raw code block + -> IO PythonScript -- ^ Code block with added capture +addPlotCapture fname content = do + absFname <- if isAbsolute fname + then (return fname) + else (</> fname) <$> getCurrentDirectory + return $ mconcat [ content + , "\nimport matplotlib.pyplot as plt" -- Just in case + , "\nplt.savefig(" <> show absFname <> ")\n\n" + ] + +-- | Detect the presence of a blocking show call, for example "plt.show()" +hasBlockingShowCall :: PythonScript -> Bool +hasBlockingShowCall script = anyOf + [ "plt.show()" `elem` scriptLines + , "matplotlib.pyplot.show()" `elem` scriptLines + ] + where + scriptLines = lines script + anyOf xs = getAny $ mconcat $ Any <$> xs
+ stack.yaml view
@@ -0,0 +1,64 @@+# This file was automatically generated by 'stack init'+#+# Some commonly used options have been documented as comments in this file.+# For advanced use and comprehensive documentation of the format, please see:+# https://docs.haskellstack.org/en/stable/yaml_configuration/++# Resolver to choose a 'specific' stackage snapshot or a compiler version.+# A snapshot resolver dictates the compiler version and the set of packages+# to be used for project dependencies. For example:+#+# resolver: lts-3.5+# resolver: nightly-2015-09-21+# resolver: ghc-7.10.2+#+# The location of a snapshot can be provided as a file or url. Stack assumes+# a snapshot provided as a file might change, whereas a url resource does not.+#+# resolver: ./custom-snapshot.yaml+# resolver: https://example.com/snapshots/2018-01-01.yaml+resolver: lts-12.10++# User packages to be built.+# Various formats can be used as shown in the example below.+#+# packages:+# - some-directory+# - https://example.com/foo/bar/baz-0.0.2.tar.gz+# - location:+# git: https://github.com/commercialhaskell/stack.git+# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a+# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a+# subdirs:+# - auto-update+# - wai+packages:+- .+# Dependency packages to be pulled from upstream that are not in the resolver+# using the same syntax as the packages field.+# (e.g., acme-missiles-0.3)+# extra-deps: []++# Override default flag values for local packages and extra-deps+# flags: {}++# Extra package databases containing global packages+# extra-package-dbs: []++# Control whether we use the GHC we find on the path+# system-ghc: true+#+# Require a specific version of stack, using version ranges+# require-stack-version: -any # Default+# require-stack-version: ">=1.10"+#+# Override the architecture used by stack, especially useful on Windows+# arch: i386+# arch: x86_64+#+# Extra directories used by stack for building+# extra-include-dirs: [/path/to/dir]+# extra-lib-dirs: [/path/to/dir]+#+# Allow a newer minor version of GHC than the snapshot specifies+# compiler-check: newer-minor