├── .appveyor.yml ├── .azure ├── azure-linux-template.yml └── azure-osx-template.yml ├── .gitattributes ├── .gitignore ├── .stylish-haskell.yaml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── Setup.hs ├── azure-pipelines.yml ├── examples ├── .pandoc-pyplot.yml ├── README.md ├── crossref.md ├── latex.tex ├── plotly.md ├── residuals.md └── style.py ├── executable ├── Main.hs └── ManPage.hs ├── format.ps1 ├── installer ├── build-setup.ps1 ├── modpath.iss └── pandoc-pyplot-setup.iss ├── pandoc-pyplot.cabal ├── src └── Text │ └── Pandoc │ └── Filter │ ├── Pyplot.hs │ └── Pyplot │ ├── Configuration.hs │ ├── FigureSpec.hs │ ├── Internal.hs │ ├── Scripting.hs │ └── Types.hs ├── stack.yaml └── test ├── Main.hs └── fixtures ├── .pandoc-pyplot.yml ├── include.py └── integration.md /.appveyor.yml: -------------------------------------------------------------------------------- 1 | # This Appveyor configuration file is modified from Stack's documentation 2 | # https://github.com/commercialhaskell/stack/blob/stable/doc/appveyor.yml 3 | # 4 | # A discussion is available here: 5 | # https://www.snoyman.com/blog/2016/08/appveyor-haskell-windows-ci 6 | # 7 | build: off 8 | 9 | cache: 10 | - "C:\\sr" 11 | 12 | 13 | # Skipping commits affecting specific files (GitHub only). 14 | # More details here: /docs/appveyor-yml 15 | skip_commits: 16 | files: 17 | - examples/* 18 | - '*.md' 19 | - azure-pipelines.yml 20 | - .azure/* 21 | 22 | clone_folder: "c:\\stack" 23 | 24 | environment: 25 | global: 26 | STACK_ROOT: "c:\\sr" 27 | 28 | # Override the temp directory to avoid sed escaping issues 29 | # See https://github.com/haskell/cabal/issues/5386 30 | TMP: "c:\\tmp" 31 | 32 | MINICONDA: "C:\\Miniconda3-x64" 33 | 34 | matrix: 35 | - ARGS: "--resolver lts-14" # GHC 8.6 36 | # - ARGS: "--resolver nightly" 37 | 38 | matrix: 39 | fast_finish: true 40 | 41 | before_test: 42 | # http://help.appveyor.com/discussions/problems/6312-curl-command-not-found 43 | - set PATH=C:\Program Files\Git\mingw64\bin;%PATH% 44 | 45 | - curl -sS -ostack.zip -L --insecure https://get.haskellstack.org/stable/windows-x86_64.zip 46 | - 7z x stack.zip stack.exe 47 | 48 | # Miniconda is preinstalled on Appveyor images 49 | # https://www.appveyor.com/docs/windows-images-software/#python 50 | - "set PATH=%MINICONDA%;%MINICONDA%\\Scripts;%PATH%" 51 | - conda update --yes -n base -c defaults conda 52 | - conda create --yes -n testenv python=3.7 53 | - activate testenv 54 | 55 | # Pillow is required to save images to jpg 56 | - conda install --yes matplotlib pillow 57 | 58 | # Plotly installation instructions are more complex because we export static images 59 | # https://plot.ly/python/static-image-export/ 60 | - conda install --yes -c plotly plotly-orca psutil requests 61 | - conda install --yes plotly 62 | 63 | test_script: 64 | 65 | # Install toolchain, but do it silently due to lots of output 66 | - stack %ARGS% setup > nul 67 | 68 | # The ugly echo "" hack is to avoid complaints about 0 being an invalid file 69 | # descriptor 70 | - echo "" | stack %ARGS% --no-terminal test --fast --ghc-options=-Werror 71 | 72 | # Integration testing 73 | # In the past, executables were built that misparsed input flags (see Issue #2) 74 | # Therefore, we have some integration test that works on some test file 75 | - echo "" | stack %ARGS% --no-terminal install . pandoc 76 | - stack %ARGS% exec -- pandoc --filter pandoc-pyplot -i ./test/fixtures/integration.md -o ./generated/test.html 77 | -------------------------------------------------------------------------------- /.azure/azure-linux-template.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | - job: ${{ parameters.name }} 3 | pool: 4 | vmImage: ${{ parameters.vmImage }} 5 | strategy: 6 | matrix: 7 | stack-lts-14: 8 | BUILD: stack 9 | ARGS: "--resolver lts-14" 10 | maxParallel: 6 11 | steps: 12 | - script: | 13 | # Pillow is required to save images to jpg 14 | python3 -m pip install --user --upgrade pip setuptools wheel 15 | python3 -m pip install --user --upgrade matplotlib 16 | python3 -m pip install --user --upgrade pillow 17 | 18 | export STACK_ROOT="$(Build.SourcesDirectory)"/.stack-root; 19 | mkdir -p ~/.local/bin 20 | curl -L https://get.haskellstack.org/stable/linux-x86_64.tar.gz | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack' 21 | case "$BUILD" in 22 | style) 23 | PACKAGE=hlint 24 | echo "Downloading $PACKAGE now ..." 25 | 26 | RELEASES=$(curl --silent https://github.com/ndmitchell/$PACKAGE/releases) 27 | URL=https://github.com/$(echo "$RELEASES" | grep -o '\"[^\"]*-x86_64-linux\.tar\.gz\"' | sed s/\"//g | head -n1) 28 | VERSION=$(echo "$URL" | sed -e 's/.*-\([\.0-9]\+\)-x86_64-linux\.tar\.gz/\1/') 29 | 30 | curl --progress-bar --location -o"$PACKAGE.tar.gz" "$URL" 31 | tar -xzf "$PACKAGE.tar.gz" -C . 32 | mv "$PACKAGE-$VERSION" "$PACKAGE" 33 | export PATH="$(pwd)"/hlint:$PATH 34 | ;; 35 | cabal) 36 | sudo add-apt-repository -y ppa:hvr/ghc 37 | sudo apt-get update 38 | sudo apt-get install cabal-install-$CABALVER ghc-$GHCVER 39 | # See note here: https://github.com/haskell-CI/haskell-ci#alex--happy-with-ghc--78 40 | if [ "$GHCVER" = "head" ] || [ "${GHCVER%.*}" = "7.8" ] || [ "${GHCVER%.*}" = "7.10" ]; then 41 | sudo apt-get install happy-1.19.4 alex-3.1.3 42 | export PATH=/opt/alex/3.1.3/bin:/opt/happy/1.19.4/bin:$PATH 43 | else 44 | sudo apt-get install happy alex 45 | fi 46 | export PATH=$HOME/.local/bin:/opt/ghc/$GHCVER/bin:/opt/cabal/$CABALVER/bin:$PATH 47 | cabal --version 48 | cabal update 49 | PACKAGES=$(stack --install-ghc query locals | grep '^ *path' | sed 's@^ *path:@@') 50 | cabal install --only-dependencies --enable-tests --enable-benchmarks --force-reinstalls --ghc-options=-O0 --reorder-goals --max-backjumps=-1 $CABALARGS $PACKAGES 51 | ;; 52 | *) 53 | export PATH=$HOME/.local/bin:$PATH 54 | stack --install-ghc $ARGS test --bench --only-dependencies 55 | ;; 56 | esac 57 | set -ex 58 | case "$BUILD" in 59 | style) 60 | hlint src/ 61 | ;; 62 | cabal) 63 | cabal install --enable-tests --enable-benchmarks --force-reinstalls --ghc-options=-O0 --reorder-goals --max-backjumps=-1 $CABALARGS $PACKAGES 64 | 65 | ORIGDIR=$(pwd) 66 | for dir in $PACKAGES 67 | do 68 | cd $dir 69 | cabal check || [ "$CABALVER" == "1.16" ] 70 | cabal sdist 71 | PKGVER=$(cabal info . | awk '{print $2;exit}') 72 | SRC_TGZ=$PKGVER.tar.gz 73 | cd dist 74 | tar zxfv "$SRC_TGZ" 75 | cd "$PKGVER" 76 | cabal configure --enable-tests --ghc-options -O0 77 | cabal build 78 | if [ "$CABALVER" = "1.16" ] || [ "$CABALVER" = "1.18" ]; then 79 | cabal test 80 | else 81 | cabal test --show-details=streaming 82 | fi 83 | cd $ORIGDIR 84 | done 85 | ;; 86 | *) 87 | stack $ARGS test --bench --no-run-benchmarks --haddock --no-haddock-deps 88 | ;; 89 | esac 90 | set +ex 91 | env: 92 | OS_NAME: ${{ parameters.os }} 93 | displayName: 'Installation ${{parameters.os}} & Test' 94 | -------------------------------------------------------------------------------- /.azure/azure-osx-template.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | - job: ${{ parameters.name }} 3 | pool: 4 | vmImage: ${{ parameters.vmImage }} 5 | strategy: 6 | matrix: 7 | stack-lts-14: 8 | BUILD: stack 9 | ARGS: "--resolver lts-14" 10 | maxParallel: 6 11 | steps: 12 | - script: | 13 | # Pillow is required to save images to jpg 14 | python3 -m pip install --upgrade pip setuptools wheel 15 | python3 -m pip install --upgrade matplotlib 16 | python3 -m pip install --upgrade pillow 17 | 18 | export STACK_ROOT="$(Build.SourcesDirectory)"/.stack-root; 19 | mkdir -p ~/.local/bin 20 | curl -skL https://get.haskellstack.org/stable/osx-x86_64.tar.gz | tar xz --strip-components=1 --include '*/stack' -C ~/.local/bin; 21 | export PATH=$HOME/.local/bin:$PATH 22 | 23 | stack --install-ghc $ARGS test --bench --only-dependencies 24 | stack $ARGS test --bench --no-run-benchmarks --haddock --no-haddock-deps 25 | env: 26 | OS_NAME: ${{ parameters.os }} 27 | displayName: 'Installation ${{parameters.os}} & Test' 28 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Ignore vendored components in language stats 5 | installer/modpath.iss linguist-vendored=true 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | dist-* 3 | cabal-dev 4 | *.o 5 | *.hi 6 | *.chi 7 | *.chs.h 8 | *.dyn_o 9 | *.dyn_hi 10 | .hpc 11 | .hsenv 12 | .cabal-sandbox/ 13 | cabal.sandbox.config 14 | *.prof 15 | *.aux 16 | *.hp 17 | *.eventlog 18 | .stack-work/ 19 | cabal.project.local 20 | cabal.project.local~ 21 | .HTF/ 22 | .ghc.environment.* 23 | 24 | examples/generated/ 25 | stack.yaml.lock 26 | -------------------------------------------------------------------------------- /.stylish-haskell.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentRDC/pandoc-pyplot/c709ec46fc4977a5f6eb223375c15889214465fd/.stylish-haskell.yaml -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | pandoc-pyplot uses [Semantic Versioning](http://semver.org/spec/v2.0.0.html) 4 | 5 | Release 2.3.0.1 6 | --------------- 7 | 8 | * Re-licensed package and library to GPL-2, same as pandoc. The previous license (MIT) was not compatible with pandoc's license. 9 | * Fixed an issue where plotly plots would not handle filenames correctly. 10 | 11 | Release 2.3.0.0 12 | --------------- 13 | 14 | * Added support for pandoc 2.8 and pandoc-types 1.20 (fixes #9). Because of breaking changes in pandoc-types 1.20, pandoc-pyplot 2.3.0.0+ only supports pandoc 2.8+. 15 | 16 | Release 2.2.0.0 17 | --------------- 18 | 19 | * Added support for rendering figures via the Plotly library. 20 | 21 | Release 2.1.5.1 22 | --------------- 23 | 24 | * Fixed an issue where setting the configuration option `transparent: true` left high-resolution figures difficult to see. Therefore, the option `transparent: true` does not affect high-resolution figures anymore. 25 | 26 | Release 2.1.5.0 27 | --------------- 28 | 29 | * Added support for two new configuration values: `tight_bbox: true|false` and `transparent: true|false`. These values are only supported via configuration files `.pandoc-pyplot.yml`. 30 | 31 | Release 2.1.4.0 32 | --------------- 33 | 34 | * Added examples and documentation on how to use `pandoc-pyplot` on LaTeX documents. 35 | * Allowed raw LaTeX macros in figure captions. This is required to label figures in LaTeX. E.g.: 36 | 37 | ```latex 38 | \begin{minted}[caption=myCaption\label{myfig}]{pyplot} 39 | 40 | \end{minted} 41 | ``` 42 | 43 | * `with-links` key changed to `links`. I'm sorry. Pandoc doesn't support LaTeX tokens with `-`. 44 | 45 | Release 2.1.3.0 46 | --------------- 47 | 48 | * Switched to using [optparse-applicative](https://github.com/pcapriotti/optparse-applicative#arguments) for command-line argument parsing. 49 | * Added a command-line options, "--write-example-config", which will write a config file ".pandoc-pyplot.yml" to show all available configuration options. 50 | * Links to source code and high-res images can be suppressed using `{.pyplot with-links=false ...}` (or via the configuration file with `with-links: false`). This is to get cleaner output in technical documentation (e.g. PDF). Example: 51 | 52 | ```markdown 53 | ```{.pyplot caption="This is a caption" with-links=false} 54 | import matplotlib.pyplot as plt 55 | plt.figure() 56 | plt.plot([1,2,3,4,5],[1,2,3,4,5]) 57 | ``` 58 | ``` 59 | * Added automated builds on macOS and Linux via Azure-Pipelines. Windows build will stay on Appveyor for now. 60 | 61 | Release 2.1.2.0 62 | --------------- 63 | 64 | * Added the "flags" configuration option, which allows to pass command-line flags to the Python interpreter. For example, warnings can be suppressed using the `-Wignore` flag. 65 | * Refactoring of the script check mechanism. It will be much easier to extend in the future. 66 | * Updated the command-line help with an example combining pandoc-pyplot and pandoc-crossref 67 | * Default Python interpreter is now "python" on Windows and __"python3" otherwise__. 68 | 69 | Release 2.1.1.1 70 | --------------- 71 | 72 | * Fixed a critical bug where pandoc-pyplot would interpret input from pandoc as a malformed command-line flag. 73 | 74 | Release 2.1.1.0 75 | --------------- 76 | 77 | * Added a command-line option to open the HTML manual in the default web browser. 78 | * Added documentation regarding compatibility with pandoc-crossref. This was always supported but not explicitly documented. 79 | 80 | Release 2.1.0.1 81 | --------------- 82 | 83 | * Fixed outdated documentation (referencing "target" parameter) 84 | * Fixed types required to build Configuration values that were not exported (SaveFormat, PythonScript) 85 | 86 | Release 2.1.0.0 87 | --------------- 88 | 89 | * Added support for config files ".pandoc-pyplot.yml", which specify different default values. This is mirrored in the new `Configuration` type and new functions, `makePlotWithConfig` and `plotTransformWithConfig`. 90 | * Added the ability to specify a different Python interpreter to use. 91 | * Added support for GIF and TIF files. 92 | * Added the "-f"/"--formats" command to show supported output figure formats. 93 | * Added support for GHC 8.2 94 | * Moved internal modules to `Text.Pandoc.Filter.Pyplot.Internal` module. 95 | 96 | Release 2.0.1.0 97 | --------------- 98 | 99 | * Support for Markdown formatting in figure captions, including LaTeX math. 100 | 101 | Release 2.0.0.0 102 | --------------- 103 | 104 | Many **breaking changes** in this release: 105 | 106 | * `pandoc-pyplot` will now determine the filename based on hashing the figure content. Therefore, figures will only be re-generated if necessary. 107 | * Removed the ability to control the filename and format directly using the `plot_target=...` attribute. 108 | * Added the ability to control the directory in which figures will be saved using the `directory=...` attribute. 109 | * Added the possibility to control the figures dots-per-inch (i.e. pixel density) with the `dpi=...` attribute. 110 | * Added the ability to control the figure format with the `format=...` attribute. Possible values are currently `"png"`, `"svg"`, `"pdf"`, `"jpg"`/`"jpeg"` and `"eps"`. 111 | * The confusing `plot_alt=...` attribute has been renamed to `caption=...` for obvious reasons. 112 | * The `plot_include=...` attribute has been renamed to `include=...`. 113 | * Added the generation of a higher resolution figure for every figure `pandoc-pyplot` understands. 114 | 115 | Release 1.1.0.0 116 | --------------- 117 | 118 | * Added the ability to include Python files before code using the `plot_include=script.py` attribute. 119 | * Added a test suite. 120 | 121 | Release 1.0.3.0 122 | --------------- 123 | 124 | * Fixed an issue where `pandoc-pyplot` would not build with base < 4.9 (#1) 125 | 126 | Release 1.0.2.0 127 | --------------- 128 | 129 | * Added support for captions using the `plot_alt=...` attribute. For example: 130 | 131 | ```markdown 132 | ```{plot_target=test.png plot_alt="This is a caption"} 133 | import matplotlib.pyplot as plt 134 | plt.figure() 135 | plt.plot([1,2,3,4,5],[1,2,3,4,5]) 136 | ``` 137 | ``` 138 | 139 | Release 1.0.1.0 140 | --------------- 141 | 142 | * Added `plotTransform :: Pandoc -> IO Pandoc` function to transform entire documents. This makes it easier to integrate `pandoc-pyplot` into Hakyll-based sites! 143 | 144 | Release 1.0.0.1 145 | --------------- 146 | 147 | * Updated README with fixes and warnings 148 | * Added top-level package documentation compatible with Haddock 149 | * Added Unsafe language extension, as this filter will run arbitrary Python scripts. 150 | 151 | Release 1.0.0.0 152 | --------------- 153 | 154 | Initial release. 155 | 156 | See documentation on [Hackage](https://hackage.haskell.org/package/pandoc-pyplot) 157 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **DEPRECATED. The [`pandoc-plot`](https://github.com/LaurentRDC/pandoc-plot) project replaces `pandoc-pyplot` by extending its capabilities to other plotting toolkits. `pandoc-pyplot` will not received any new features.** 2 | 3 | # pandoc-pyplot - A Pandoc filter to generate Matplotlib/Plotly figures directly in documents 4 | 5 | [![Hackage version](https://img.shields.io/hackage/v/pandoc-pyplot.svg)](http://hackage.haskell.org/package/pandoc-pyplot) [![Stackage version (LTS)](http://stackage.org/package/pandoc-pyplot/badge/lts)](http://stackage.org/nightly/package/pandoc-pyplot) [![Windows Build status](https://ci.appveyor.com/api/projects/status/qbmq9cyks5jup48e?svg=true)](https://ci.appveyor.com/project/LaurentRDC/pandoc-pyplot) [![macOS and Linux Build Status](https://dev.azure.com/laurentdecotret/pandoc-pyplot/_apis/build/status/LaurentRDC.pandoc-pyplot?branchName=master)](https://dev.azure.com/laurentdecotret/pandoc-pyplot/_build/latest?definitionId=2&branchName=master) 6 | ![GitHub](https://img.shields.io/github/license/LaurentRDC/pandoc-pyplot.svg) 7 | 8 | `pandoc-pyplot` turns Python code present in your documents into embedded figures via Matplotlib or Plotly. 9 | 10 | 11 | * [Usage](#usage) 12 | * [Markdown](#markdown) 13 | * [LaTeX](#latex) 14 | * [Examples](#examples) 15 | * [Features](#features) 16 | * [Captions](#captions) 17 | * [Link to source code and high-resolution 18 | figure](#link-to-source-code-and-high-resolution-figure) 19 | * [Including scripts](#including-scripts) 20 | * [Multiple backends](#multiple-backends) 21 | * [No wasted work](#no-wasted-work) 22 | * [Compatibility with 23 | pandoc-crossref](#compatibility-with-pandoc-crossref) 24 | * [Configurable](#configurable) 25 | * [Configuration-only parameters](#configuration-only-parameters) 26 | * [Installation](#installation) 27 | * [Running the filter](#running-the-filter) 28 | * [Usage as a Haskell library](#usage-as-a-haskell-library) 29 | * [Usage with Hakyll](#usage-with-hakyll) 30 | * [Warning](#warning) 31 | 32 | ## Usage 33 | 34 | ### Markdown 35 | 36 | 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. 37 | 38 | Here is a basic example using the scripting `matplotlib.pyplot` API: 39 | 40 | ~~~markdown 41 | ```{.pyplot} 42 | import matplotlib.pyplot as plt 43 | 44 | plt.figure() 45 | plt.plot([0,1,2,3,4], [1,2,3,4,5]) 46 | plt.title('This is an example figure') 47 | ``` 48 | ~~~ 49 | 50 | Putting the above in `input.md`, we can then generate the plot and embed it: 51 | 52 | ```bash 53 | pandoc --filter pandoc-pyplot input.md --output output.html 54 | ``` 55 | 56 | or 57 | 58 | ```bash 59 | pandoc --filter pandoc-pyplot input.md --output output.pdf 60 | ``` 61 | 62 | or any other output format you want. 63 | 64 | ### LaTeX 65 | 66 | The filter works slightly differently in LaTeX documents. In LaTeX, the `minted` environment must be used, with the `pyplot` class. 67 | 68 | ```latex 69 | \begin{minted}{pyplot} 70 | import matplotlib.pyplot as plt 71 | 72 | plt.figure() 73 | plt.plot([0,1,2,3,4], [1,2,3,4,5]) 74 | plt.title('This is an example figure') 75 | \end{minted} 76 | ``` 77 | 78 | Note that __you do not need to have `minted` installed__. 79 | 80 | ### Examples 81 | 82 | There are more examples in the [source repository](https://github.com/LaurentRDC/pandoc-pyplot), in the `\examples` directory. 83 | 84 | ## Features 85 | 86 | ### Captions 87 | 88 | You can also specify a caption for your image. This is done using the optional `caption` parameter. 89 | 90 | __Markdown__: 91 | 92 | ~~~markdown 93 | ```{.pyplot caption="This is a simple figure"} 94 | import matplotlib.pyplot as plt 95 | 96 | plt.figure() 97 | plt.plot([0,1,2,3,4], [1,2,3,4,5]) 98 | plt.title('This is an example figure') 99 | ``` 100 | ~~~ 101 | 102 | __LaTex__: 103 | 104 | ```latex 105 | \begin{minted}[caption=This is a simple figure]{pyplot} 106 | import matplotlib.pyplot as plt 107 | 108 | plt.figure() 109 | plt.plot([0,1,2,3,4], [1,2,3,4,5]) 110 | plt.title('This is an example figure') 111 | \end{minted} 112 | ``` 113 | 114 | Caption formatting is either plain text or Markdown. LaTeX-style math is also support in captions (using dollar signs $...$). 115 | 116 | ### Link to source code and high-resolution figure 117 | 118 | 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. A high resolution image will be made available in a caption link. 119 | 120 | (*New in version 2.1.3.0*) For cleaner output (e.g. PDF), you can turn this off via the `links=false` key: 121 | 122 | __Markdown__: 123 | 124 | ~~~markdown 125 | ```{.pyplot links=false} 126 | ... 127 | ``` 128 | ~~~ 129 | 130 | __LaTex__: 131 | 132 | ```latex 133 | \begin{minted}[links=false]{pyplot} 134 | ... 135 | \end{minted} 136 | ``` 137 | 138 | or via a [configuration file](#Configurable). 139 | 140 | ### Including scripts 141 | 142 | If you find yourself always repeating some steps, inclusion of scripts is possible using the `include` parameter. For example, if you want all plots to have the [`ggplot`](https://matplotlib.org/tutorials/introductory/customizing.html#sphx-glr-tutorials-introductory-customizing-py) style, you can write a very short preamble `style.py` like so: 143 | 144 | ```python 145 | import matplotlib.pyplot as plt 146 | plt.style.use('ggplot') 147 | ``` 148 | 149 | and include it in your document as follows: 150 | 151 | ~~~markdown 152 | ```{.pyplot include=style.py} 153 | plt.figure() 154 | plt.plot([0,1,2,3,4], [1,2,3,4,5]) 155 | plt.title('This is an example figure') 156 | ``` 157 | ~~~ 158 | 159 | Which is equivalent to writing the following markdown: 160 | 161 | ~~~markdown 162 | ```{.pyplot} 163 | import matplotlib.pyplot as plt 164 | plt.style.use('ggplot') 165 | 166 | plt.figure() 167 | plt.plot([0,1,2,3,4], [1,2,3,4,5]) 168 | plt.title('This is an example figure') 169 | ``` 170 | ~~~ 171 | 172 | The equivalent LaTeX usage is as follows: 173 | 174 | ```latex 175 | \begin{minted}[include=style.py]{pyplot} 176 | 177 | \end{minted} 178 | ``` 179 | 180 | 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. 181 | 182 | 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`: 183 | ```python 184 | import matplotlib.pyplot as plt 185 | 186 | plt.rcParams['savefig.facecolor'] = 'k' 187 | ... 188 | ``` 189 | You can take a look at all available `matplotlib` parameters [here](https://matplotlib.org/users/customizing.html). 190 | 191 | ### Multiple backends 192 | 193 | (*new in version 2.2.0.0*) Both Matplotlib and Plotly are supported! 194 | 195 | To render Plotly figures in Markdown: 196 | 197 | ~~~markdown 198 | ```{.plotly caption="This is a Plotly figure"} 199 | import plotly.graph_objects as go 200 | figure = go.Figure( 201 | data=[go.Bar(y=[2, 1, 3])], 202 | ) 203 | ~~~ 204 | 205 | Here is the LaTeX equivalent: 206 | 207 | ```latex 208 | \begin{minted}[caption=This is a Plotly figure]{plotly} 209 | import plotly.graph_objects as go 210 | figure = go.Figure( 211 | data=[go.Bar(y=[2, 1, 3])], 212 | ) 213 | \end{minted} 214 | ``` 215 | 216 | `pandoc-pyplot` will render and capture your figure automagically. 217 | 218 | ### No wasted work 219 | 220 | `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. 221 | 222 | ### Compatibility with pandoc-crossref 223 | 224 | [`pandoc-crossref`](https://github.com/lierdakil/pandoc-crossref) is a pandoc filter that makes it effortless to cross-reference objects in Markdown documents. 225 | 226 | You can use `pandoc-crossref` in conjunction with `pandoc-pyplot` for the ultimate figure-making pipeline. You can combine both in a figure like so: 227 | 228 | ~~~markdown 229 | ```{#fig:myexample .pyplot caption="This is a caption"} 230 | # Insert figure script here 231 | ``` 232 | 233 | As you can see in @fig:myexample, ... 234 | ~~~ 235 | 236 | If the above source is located in file `myfile.md`, you can render the figure and references by applying `pandoc-pyplot` **first**, and then `pandoc-crossref`. For example: 237 | 238 | ```bash 239 | pandoc --filter pandoc-pyplot --filter pandoc-crossref -i myfile.md -o myfile.html 240 | ``` 241 | 242 | ### Configurable 243 | 244 | (*New in version 2.1.0.0*) To avoid repetition, `pandoc-pyplot` can be configured using simple YAML files. `pandoc-pyplot` will look for a `.pandoc-pyplot.yml` file in the current working directory: 245 | 246 | ```yaml 247 | # You can specify any or all of the following parameters 248 | interpreter: python36 249 | directory: mydirectory/ 250 | include: mystyle.py 251 | format: jpeg 252 | links: false 253 | dpi: 150 # Matplotlib only 254 | tight_bbox: true # Matplotlib only 255 | transparent: false # Matplotlib only 256 | flags: [-O, -Wignore] 257 | ``` 258 | 259 | These values override the default values, which are equivalent to: 260 | 261 | ```yaml 262 | # Defaults if no configuration is provided. 263 | # Note that the default interpreter name on MacOS and Unix is 'python3' 264 | # and 'python' on Windows. 265 | interpreter: python 266 | flags: [] 267 | directory: generated/ 268 | format: png 269 | links: true 270 | dpi: 80 271 | tight_bbox: false 272 | transparent: false 273 | ``` 274 | 275 | Using `pandoc-pyplot --write-example-config` will write the default configuration to a file `.pandoc-pyplot.yml`, which you can then customize. 276 | 277 | #### Configuration-only parameters 278 | 279 | There are a few parameters that are __only__ available via the configuration file `.pandoc-pyplot.yml`: 280 | 281 | * `interpreter` is the name of the interpreter to use. For example, `interpreter: python36`; 282 | * `flags` is a list of strings, which are flags that are passed to the python interpreter. For example, `flags: [-O, -Wignore]`; 283 | * (*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. 284 | * (*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. 285 | 286 | ## Installation 287 | 288 | ### Binaries 289 | 290 | Windows binaries are available on [GitHub](https://github.com/LaurentRDC/pandoc-pyplot/releases). Place the executable in a location that is in your PATH to be able to call it. 291 | 292 | If you can show me how to generate binaries for other platform using e.g. Azure Pipelines, let me know! 293 | 294 | ### Installers (Windows) 295 | 296 | Windows installers are made available thanks to [Inno Setup](http://www.jrsoftware.org/isinfo.php). You can download them from the [release page](https://github.com/LaurentRDC/pandoc-pyplot/releases/latest). 297 | 298 | ### From Hackage/Stackage 299 | 300 | `pandoc-pyplot` is available on Hackage. Using the [`cabal-install`](https://www.haskell.org/cabal/) tool: 301 | 302 | ```bash 303 | cabal update 304 | cabal install pandoc-pyplot 305 | ``` 306 | 307 | Similarly, `pandoc-pyplot` is available on Stackage: 308 | 309 | ```bash 310 | stack update 311 | stack install pandoc-pyplot 312 | ``` 313 | 314 | ### From source 315 | 316 | Building from source can be done using [`stack`](https://docs.haskellstack.org/en/stable/README/) or [`cabal`](https://www.haskell.org/cabal/): 317 | 318 | ```bash 319 | git clone https://github.com/LaurentRDC/pandoc-pyplot 320 | cd pandoc-pylot 321 | stack install # Alternatively, `cabal install` 322 | ``` 323 | 324 | ## Running the filter 325 | 326 | ### Requirements 327 | 328 | 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. 329 | 330 | Use the filter with Pandoc as follows: 331 | 332 | ```bash 333 | pandoc --filter pandoc-pyplot input.md --output output.html 334 | ``` 335 | 336 | in which case, the output is HTML. Another example with PDF output: 337 | 338 | ```bash 339 | pandoc --filter pandoc-pyplot input.md --output output.pdf 340 | ``` 341 | 342 | Python exceptions will be printed to screen in case of a problem. 343 | 344 | `pandoc-pyplot` has a limited command-line interface. Take a look at the help available using the `-h` or `--help` argument: 345 | 346 | ```bash 347 | pandoc-pyplot --help 348 | ``` 349 | 350 | ## Usage as a Haskell library 351 | 352 | 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). 353 | 354 | ### Usage with Hakyll 355 | 356 | 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 can use a transform function that works on entire documents: 357 | 358 | ```haskell 359 | import Text.Pandoc.Filter.Pyplot (plotTransform) 360 | 361 | import Hakyll 362 | 363 | -- Unsafe compiler is required because of the interaction 364 | -- in IO (i.e. running an external Python script). 365 | makePlotPandocCompiler :: Compiler (Item String) 366 | makePlotPandocCompiler = 367 | pandocCompilerWithTransformM 368 | defaultHakyllReaderOptions 369 | defaultHakyllWriterOptions 370 | (unsafeCompiler . plotTransform) 371 | ``` 372 | 373 | The `plotTransformWithConfig` is also available for a more configurable set-up. 374 | 375 | ## Warning 376 | 377 | Do not run this filter on unknown documents. There is nothing in `pandoc-pyplot` that can stop a Python script from performing **evil actions**. 378 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | -- This script is used to build and install your package. Typically you don't 2 | -- need to change it. The Cabal documentation has more information about this 3 | -- file: . 4 | import qualified Distribution.Simple 5 | 6 | main :: IO () 7 | main = Distribution.Simple.defaultMain 8 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # This is the complex Azure configuration, which is intended for use 2 | # on open source libraries which need compatibility across multiple GHC 3 | # versions, must work with cabal-install, and should be 4 | # cross-platform. For more information and other options, see: 5 | # 6 | # https://docs.haskellstack.org/en/stable/azure_ci/ 7 | # 8 | # Copy these contents into the root directory of your Github project in a file 9 | # named azure-pipelines.yml 10 | # 11 | # For better organization, you split various jobs into seprate parts 12 | # and each of them are controlled via individual file. 13 | jobs: 14 | - template: ./.azure/azure-linux-template.yml 15 | parameters: 16 | name: Linux 17 | vmImage: ubuntu-16.04 18 | os: linux 19 | 20 | - template: ./.azure/azure-osx-template.yml 21 | parameters: 22 | name: macOS 23 | vmImage: macOS-10.13 24 | os: osx -------------------------------------------------------------------------------- /examples/.pandoc-pyplot.yml: -------------------------------------------------------------------------------- 1 | directory: generated/other 2 | format: jpg 3 | dpi: 150 -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples of pandoc-pyplot usage 2 | 3 | This folder contains everything you need to get started with two examples of pandoc-pyplot. These examples showcase a few features of pandoc-pyplot: 4 | 5 | * Regular captions; 6 | * Markdown captions with LaTeX math; 7 | * Include scripts; 8 | * Interaction with other Pandoc filters; 9 | * YAML configuration. 10 | 11 | ## Example 1: simple pandoc-pyplot plotting 12 | 13 | The first example is located in file `residuals.md`. Note that default values are determined bu the `.pandoc-pyplot.yml` config file. 14 | 15 | The easiest way to compile this example is to HTML: 16 | 17 | ``` 18 | pandoc --filter pandoc-pyplot -i residuals.md -o residuals.html 19 | ``` 20 | 21 | If you have a LaTeX toolchain installed, you can generate a PDF as well: 22 | 23 | ``` 24 | pandoc --filter pandoc-pyplot -i residuals.md -o residuals.pdf 25 | ``` 26 | 27 | ## Example 2: pandoc-pyplot and pandoc-crossref together 28 | 29 | The second example showcases the interactions between pandoc-pyplot and pandoc-crossref. It is located in the file `crossref.md`. For this example to work, pandoc-pyplot must be used __first__. Note that default values are determined by the `.pandoc-pyplot.yml` config file. 30 | 31 | The easiest way to compile this example is to HTML: 32 | 33 | ```bash 34 | pandoc --filter pandoc-pyplot --filter pandoc-crossref -i crossref.md -o crossref.html 35 | ``` 36 | 37 | If you have a LaTeX toolchain installed, you can generate a PDF as well: 38 | 39 | ``` 40 | pandoc --filter pandoc-pyplot --filter pandoc-crossref -i crossref.md -o crossref.pdf 41 | ``` 42 | 43 | ## Example 3 : pandoc-pyplot and LaTeX 44 | 45 | This third example demonstrates how pandoc-pyplot can be included in a LaTeX pipeline. It is recommended that the figures first be rendered "in-place": 46 | 47 | ```bash 48 | pandoc --filter pandoc-pyplot -i latex.tex -o latex_with_figures.tex 49 | ``` 50 | 51 | and then your usual LaTeX -> PDF rendering happens. The intermediate file will not be human-readable, most probably. 52 | 53 | To label a figure, you can use raw TeX macros in captions (requires pandoc-pyplot > 2.1.4.0): 54 | 55 | ```latex 56 | \begin{minted}[caption=This is an example\label{example} include=style.py, format=png]{pyplot} 57 | ... 58 | \end{minted} 59 | 60 | ... as seen in Figure \ref{example}. 61 | ``` -------------------------------------------------------------------------------- /examples/crossref.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Compatibility with pandoc-crossref 3 | --- 4 | 5 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 6 | 7 | ```{#fig:myfigure .pyplot include="style.py" caption="This is a caption"} 8 | import matplotlib.pyplot as plt 9 | import numpy as np 10 | 11 | x = np.linspace(0, 10, 1024) 12 | y = 10*np.sin(2*np.pi*x) 13 | n = np.random.random(size = x.shape) - 0.5 14 | 15 | # First create an empty figure 16 | plt.figure() 17 | 18 | # Plot both noisy signal and expected sinusoid 19 | plt.plot(x, y + n, 'r.') 20 | plt.plot(x, y, 'k-') 21 | 22 | # Plot formatting 23 | plt.xlabel('Abcissa') 24 | plt.ylabel('Ordinates') 25 | plt.title('Sinusoid') 26 | ``` 27 | 28 | As you can see in @fig:myfigure, pandoc-crossref is compatible with the ouput of pandoc-pyplot. -------------------------------------------------------------------------------- /examples/latex.tex: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 2 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 3 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 4 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 5 | velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 6 | occaecat cupidatat non proident, sunt in culpa qui officia deserunt 7 | mollit anim id est laborum. 8 | 9 | \begin{minted}[caption=This is an example from Matplotlib's gallery, include=style.py, format=png]{pyplot} 10 | # Matplotlib Gallery item available below: 11 | # 12 | # https://matplotlib.org/examples/images_contours_and_fields/pcolormesh_levels.html 13 | import matplotlib.pyplot as plt 14 | from matplotlib.colors import BoundaryNorm 15 | from matplotlib.ticker import MaxNLocator 16 | import numpy as np 17 | 18 | 19 | # make these smaller to increase the resolution 20 | dx, dy = 0.05, 0.05 21 | 22 | # generate 2 2d grids for the x & y bounds 23 | y, x = np.mgrid[slice(1, 5 + dy, dy), 24 | slice(1, 5 + dx, dx)] 25 | 26 | z = np.sin(x)**10 + np.cos(10 + y*x) * np.cos(x) 27 | 28 | # x and y are bounds, so z should be the value *inside* those bounds. 29 | # Therefore, remove the last value from the z array. 30 | z = z[:-1, :-1] 31 | levels = MaxNLocator(nbins=15).tick_values(z.min(), z.max()) 32 | 33 | 34 | # pick the desired colormap, sensible levels, and define a normalization 35 | # instance which takes data values and translates those into levels. 36 | cmap = plt.get_cmap('PiYG') 37 | norm = BoundaryNorm(levels, ncolors=cmap.N, clip=True) 38 | 39 | fig, (ax0, ax1) = plt.subplots(nrows=2) 40 | 41 | im = ax0.pcolormesh(x, y, z, cmap=cmap, norm=norm) 42 | fig.colorbar(im, ax=ax0) 43 | ax0.set_title('pcolormesh with levels') 44 | 45 | 46 | # contours are *point* based plots, so convert our bound into point 47 | # centers 48 | cf = ax1.contourf(x[:-1, :-1] + dx/2., 49 | y[:-1, :-1] + dy/2., z, levels=levels, 50 | cmap=cmap) 51 | fig.colorbar(cf, ax=ax1) 52 | ax1.set_title('contourf with levels') 53 | 54 | # adjust spacing between subplots so `ax1` title and `ax0` tick labels 55 | # don't overlap 56 | fig.tight_layout() 57 | \end{minted} 58 | 59 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 60 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 61 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 62 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 63 | velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 64 | occaecat cupidatat non proident, sunt in culpa qui officia deserunt 65 | mollit anim id est laborum. -------------------------------------------------------------------------------- /examples/plotly.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Example of pandoc-pyplot working with Plotly 3 | --- 4 | 5 | This example shows a document with Python figures that can be rendered with Plotly. 6 | 7 | ```{.plotly caption="Example taken from Plotly's tutorial"} 8 | import plotly.graph_objects as go 9 | import numpy as np 10 | np.random.seed(1) 11 | 12 | N = 100 13 | x = np.random.rand(N) 14 | y = np.random.rand(N) 15 | colors = np.random.rand(N) 16 | sz = np.random.rand(N) * 30 17 | 18 | # Inside a {.plotly} clause, you can only have one Figure. 19 | # pandoc-pyplot will automagically find it, no matter its name, and save 20 | # it to file 21 | fig = go.Figure() 22 | fig.add_trace(go.Scatter( 23 | x=x, 24 | y=y, 25 | mode="markers", 26 | marker=go.scatter.Marker( 27 | size=sz, 28 | color=colors, 29 | opacity=0.6, 30 | colorscale="Viridis" 31 | ) 32 | )) 33 | ``` -------------------------------------------------------------------------------- /examples/residuals.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: test of pandoc-pyplot 3 | date: 2018-09-28 4 | --- 5 | 6 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 7 | 8 | ```{.pyplot} 9 | import matplotlib.pyplot as plt 10 | import numpy as np 11 | 12 | x = np.linspace(0, 10, 1024) 13 | y = 10*np.sin(2*np.pi*x) 14 | n = np.random.random(size = x.shape) - 0.5 15 | 16 | # First create an empty figure 17 | plt.figure() 18 | 19 | # Plot both noisy signal and expected sinusoid 20 | plt.plot(x, y + n, 'r.') 21 | plt.plot(x, y, 'k-') 22 | 23 | # Plot formatting 24 | plt.xlabel('Abcissa') 25 | plt.ylabel('Ordinates') 26 | plt.title('Sinusoid') 27 | ``` 28 | 29 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 30 | 31 | ```{.pyplot caption="This is a **caption** with *Markdown* formatting. It also includes math symbols like $\alpha$ and $\beta$"} 32 | import matplotlib.pyplot as plt 33 | import numpy as np 34 | 35 | x = np.linspace(0, 10, 1024) 36 | y = 10*np.sin(2*np.pi*x) 37 | n = np.random.random(size = x.shape) - 0.5 38 | 39 | # First create an empty figure 40 | fig, (ax1, ax2) = plt.subplots(nrows=2, ncols=1, sharex=True) 41 | 42 | # Plot both noisy signal and expected sinusoid 43 | ax1.plot(x, y + n, 'g.') 44 | ax1.plot(x, y, 'k-') 45 | 46 | # Residuals 47 | ax2.plot(x, n, '.g') 48 | ax2.axhline(y=0, color='k') 49 | 50 | # Plot formatting 51 | ax2.set_xlabel('Abcissa') 52 | ax1.set_title('Residuals') 53 | ``` 54 | 55 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 56 | 57 | ```{.pyplot include=style.py caption="This plot has a different style. It also omits links to source code; perfect for PDF output!" links=false} 58 | import numpy as np 59 | 60 | x = np.linspace(0, 10, 1024) 61 | y = 10*np.cos(np.pi*x) 62 | n = (np.random.random(size = x.shape) - 0.5) * 5 63 | 64 | # First create an empty figure 65 | fig, (ax1, ax2) = plt.subplots(nrows=2, ncols=1, sharex=True) 66 | 67 | # Plot both noisy signal and expected sinusoid 68 | ax1.plot(x, y + n, 'b.') 69 | ax1.plot(x, y, 'k-') 70 | 71 | # Residuals 72 | ax2.plot(x, n, '.b') 73 | ax2.axhline(y=0, color='k') 74 | 75 | # Plot formatting 76 | ax2.set_xlabel('Abcissa') 77 | ax1.set_title('Residuals in ggplot style') 78 | ``` -------------------------------------------------------------------------------- /examples/style.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | plt.style.use('ggplot') 3 | -------------------------------------------------------------------------------- /executable/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ApplicativeDo #-} 2 | {-# LANGUAGE LambdaCase #-} 3 | {-# LANGUAGE TemplateHaskell #-} 4 | 5 | module Main where 6 | 7 | import Control.Applicative ((<|>)) 8 | import Control.Monad (join) 9 | 10 | import Data.Default.Class (def) 11 | import Data.List (intersperse) 12 | import Data.Monoid ((<>)) 13 | import qualified Data.Text as T 14 | 15 | import Options.Applicative 16 | import qualified Options.Applicative.Help.Pretty as P 17 | 18 | import System.Directory (doesFileExist) 19 | import System.IO.Temp (writeSystemTempFile) 20 | 21 | import Text.Pandoc.Filter.Pyplot (SaveFormat (..), 22 | configuration, 23 | plotTransformWithConfig) 24 | import Text.Pandoc.Filter.Pyplot.Internal (writeConfig) 25 | import Text.Pandoc.JSON (toJSONFilter) 26 | 27 | import Web.Browser (openBrowser) 28 | 29 | import qualified Data.Version as V 30 | import Paths_pandoc_pyplot (version) 31 | 32 | import ManPage (embedManualHtml) 33 | 34 | main :: IO () 35 | main = join $ execParser opts 36 | where 37 | opts = info (run <**> helper) 38 | (fullDesc 39 | <> progDesc "This pandoc filter generates plots from Python code blocks using Matplotlib. This allows to keep documentation and figures in perfect synchronicity." 40 | <> header "pandoc-pyplot - generate Matplotlib figures directly in documents." 41 | <> footerDoc (Just footer') 42 | ) 43 | 44 | 45 | toJSONFilterWithConfig :: IO () 46 | toJSONFilterWithConfig = do 47 | configExists <- doesFileExist ".pandoc-pyplot.yml" 48 | config <- if configExists 49 | then configuration ".pandoc-pyplot.yml" 50 | else def 51 | toJSONFilter (plotTransformWithConfig config) 52 | 53 | 54 | data Flag = Version 55 | | Formats 56 | | Manual 57 | | Config 58 | deriving (Eq) 59 | 60 | 61 | run :: Parser (IO ()) 62 | run = do 63 | versionP <- flag Nothing (Just Version) (long "version" <> short 'v' 64 | <> help "Show version number and exit.") 65 | 66 | formatsP <- flag Nothing (Just Formats) (long "formats" <> short 'f' 67 | <> help "Show supported output figure formats and exit.") 68 | 69 | manualP <- flag Nothing (Just Manual) (long "manual" <> short 'm' 70 | <> help "Open the manual page in the default web browser and exit.") 71 | 72 | configP <- flag Nothing (Just Config) (long "write-example-config" 73 | <> help "Write the default configuration in '.pandoc-pyplot.yml', \ 74 | \which you can subsequently customize, and exit. If '.pandoc-pyplot.yml' \ 75 | \already exists, an error will be thrown. ") 76 | 77 | input <- optional $ strArgument (metavar "AST") 78 | return $ go (versionP <|> formatsP <|> manualP <|> configP) input 79 | where 80 | go :: Maybe Flag -> Maybe String -> IO () 81 | go (Just Version) _ = putStrLn (V.showVersion version) 82 | go (Just Formats) _ = putStrLn . mconcat . intersperse ", " . fmap show $ supportedSaveFormats 83 | go (Just Manual) _ = writeSystemTempFile "pandoc-pyplot-manual.html" (T.unpack manualHtml) 84 | >>= \fp -> openBrowser ("file:///" <> fp) 85 | >> return () 86 | go (Just Config) _ = writeConfig ".pandoc-pyplot.yml" def 87 | go Nothing _ = toJSONFilterWithConfig 88 | 89 | 90 | supportedSaveFormats :: [SaveFormat] 91 | supportedSaveFormats = enumFromTo minBound maxBound 92 | 93 | 94 | manualHtml :: T.Text 95 | manualHtml = T.pack $(embedManualHtml) 96 | 97 | 98 | -- | Use Doc type directly because of newline formatting 99 | footer' :: P.Doc 100 | footer' = mconcat [ 101 | P.text "Example usage with pandoc:" 102 | , P.line, P.line 103 | , P.indent 4 $ P.string "> pandoc --filter pandoc-pyplot input.md --output output.html" 104 | , P.line, P.line 105 | , P.text "If you use pandoc-pyplot in combination with other filters, you probably want to run pandoc-pyplot first. Here is an example with pandoc-crossref:" 106 | , P.line, P.line 107 | , P.indent 4 $ P.string "> pandoc --filter pandoc-pyplot --filter pandoc-crossref -i input.md -o output.pdf" 108 | , P.line, P.line 109 | , P.text "More information can be found via the manual (pandoc-pyplot --manual) or the repository README, located at" 110 | , P.line 111 | , P.indent 4 $ P.text "https://github.com/LaurentRDC/pandoc-pyplot" 112 | , P.line 113 | ] 114 | -------------------------------------------------------------------------------- /executable/ManPage.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TemplateHaskellQuotes #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-| 4 | This module was inspired by pandoc-crossref 5 | |-} 6 | 7 | module ManPage ( embedManualHtml ) where 8 | 9 | import Control.DeepSeq (($!!)) 10 | 11 | import Data.String 12 | import qualified Data.Text as T 13 | 14 | import Language.Haskell.TH.Syntax 15 | 16 | import qualified Text.Pandoc as P 17 | import Text.Pandoc.Highlighting (pygments) 18 | 19 | import System.FilePath (FilePath) 20 | import System.IO 21 | 22 | docFile :: FilePath 23 | docFile = "README.md" 24 | 25 | readDocFile :: IO String 26 | readDocFile = withFile docFile ReadMode $ \h -> do 27 | hSetEncoding h utf8 28 | cont <- hGetContents h 29 | return $!! cont 30 | 31 | readerOpts :: P.ReaderOptions 32 | readerOpts = P.def { P.readerExtensions = P.githubMarkdownExtensions 33 | , P.readerStandalone = True 34 | } 35 | 36 | embedManual :: (P.Pandoc -> P.PandocPure T.Text) -> Q Exp 37 | embedManual fmt = do 38 | qAddDependentFile docFile 39 | d <- runIO readDocFile 40 | let pd = either (error . show) id $ P.runPure $ P.readMarkdown readerOpts (T.pack d) 41 | txt = either (error . show) id $ P.runPure $ fmt pd 42 | strToExp $ T.unpack txt 43 | where 44 | strToExp :: String -> Q Exp 45 | strToExp s = return $ VarE 'fromString `AppE` LitE (StringL s) 46 | 47 | embedManualHtml :: Q Exp 48 | embedManualHtml = do 49 | embedManual $ P.writeHtml5String P.def { P.writerHighlightStyle = Just pygments } 50 | -------------------------------------------------------------------------------- /format.ps1: -------------------------------------------------------------------------------- 1 | Get-ChildItem -Path . -Recurse -Include "*.hs" | ForEach-Object{stylish-haskell -i $_.FullName} -------------------------------------------------------------------------------- /installer/build-setup.ps1: -------------------------------------------------------------------------------- 1 | "Build executable and move here" 2 | stack install pandoc-pyplot --local-bin-path ".\installer" 3 | 4 | Write-Host "Building setup using Inno Setup Compiler" 5 | if ($ENV:PROCESSOR_ARCHITECTURE -eq "AMD64"){ 6 | $iscc = get-item "C:\Program Files (x86)\Inno Setup 5\ISCC.exe" 7 | } 8 | else { 9 | $iscc = get-item "C:\Program Files\Inno Setup 5\ISCC.exe" 10 | } 11 | & $iscc "pandoc-pyplot-setup.iss" -------------------------------------------------------------------------------- /installer/modpath.iss: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // 3 | // Inno Setup Ver: 5.4.2 4 | // Script Version: 1.4.2 5 | // Author: Jared Breland 6 | // Homepage: http://www.legroom.net/software 7 | // License: GNU Lesser General Public License (LGPL), version 3 8 | // http://www.gnu.org/licenses/lgpl.html 9 | // 10 | // Script Function: 11 | // Allow modification of environmental path directly from Inno Setup installers 12 | // 13 | // Instructions: 14 | // Copy modpath.iss to the same directory as your setup script 15 | // 16 | // Add this statement to your [Setup] section 17 | // ChangesEnvironment=true 18 | // 19 | // Add this statement to your [Tasks] section 20 | // You can change the Description or Flags 21 | // You can change the Name, but it must match the ModPathName setting below 22 | // Name: modifypath; Description: &Add application directory to your environmental path; Flags: unchecked 23 | // 24 | // Add the following to the end of your [Code] section 25 | // ModPathName defines the name of the task defined above 26 | // ModPathType defines whether the 'user' or 'system' path will be modified; 27 | // this will default to user if anything other than system is set 28 | // setArrayLength must specify the total number of dirs to be added 29 | // Result[0] contains first directory, Result[1] contains second, etc. 30 | // const 31 | // ModPathName = 'modifypath'; 32 | // ModPathType = 'user'; 33 | // 34 | // function ModPathDir(): TArrayOfString; 35 | // begin 36 | // setArrayLength(Result, 1); 37 | // Result[0] := ExpandConstant('{app}'); 38 | // end; 39 | // #include "modpath.iss" 40 | // ---------------------------------------------------------------------------- 41 | 42 | procedure ModPath(); 43 | var 44 | oldpath: String; 45 | newpath: String; 46 | updatepath: Boolean; 47 | pathArr: TArrayOfString; 48 | aExecFile: String; 49 | aExecArr: TArrayOfString; 50 | i, d: Integer; 51 | pathdir: TArrayOfString; 52 | regroot: Integer; 53 | regpath: String; 54 | 55 | begin 56 | // Get constants from main script and adjust behavior accordingly 57 | // ModPathType MUST be 'system' or 'user'; force 'user' if invalid 58 | if ModPathType = 'system' then begin 59 | regroot := HKEY_LOCAL_MACHINE; 60 | regpath := 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'; 61 | end else begin 62 | regroot := HKEY_CURRENT_USER; 63 | regpath := 'Environment'; 64 | end; 65 | 66 | // Get array of new directories and act on each individually 67 | pathdir := ModPathDir(); 68 | for d := 0 to GetArrayLength(pathdir)-1 do begin 69 | updatepath := true; 70 | 71 | // Modify WinNT path 72 | if UsingWinNT() = true then begin 73 | 74 | // Get current path, split into an array 75 | RegQueryStringValue(regroot, regpath, 'Path', oldpath); 76 | oldpath := oldpath + ';'; 77 | i := 0; 78 | 79 | while (Pos(';', oldpath) > 0) do begin 80 | SetArrayLength(pathArr, i+1); 81 | pathArr[i] := Copy(oldpath, 0, Pos(';', oldpath)-1); 82 | oldpath := Copy(oldpath, Pos(';', oldpath)+1, Length(oldpath)); 83 | i := i + 1; 84 | 85 | // Check if current directory matches app dir 86 | if pathdir[d] = pathArr[i-1] then begin 87 | // if uninstalling, remove dir from path 88 | if IsUninstaller() = true then begin 89 | continue; 90 | // if installing, flag that dir already exists in path 91 | end else begin 92 | updatepath := false; 93 | end; 94 | end; 95 | 96 | // Add current directory to new path 97 | if i = 1 then begin 98 | newpath := pathArr[i-1]; 99 | end else begin 100 | newpath := newpath + ';' + pathArr[i-1]; 101 | end; 102 | end; 103 | 104 | // Append app dir to path if not already included 105 | if (IsUninstaller() = false) AND (updatepath = true) then 106 | newpath := newpath + ';' + pathdir[d]; 107 | 108 | // Write new path 109 | RegWriteStringValue(regroot, regpath, 'Path', newpath); 110 | 111 | // Modify Win9x path 112 | end else begin 113 | 114 | // Convert to shortened dirname 115 | pathdir[d] := GetShortName(pathdir[d]); 116 | 117 | // If autoexec.bat exists, check if app dir already exists in path 118 | aExecFile := 'C:\AUTOEXEC.BAT'; 119 | if FileExists(aExecFile) then begin 120 | LoadStringsFromFile(aExecFile, aExecArr); 121 | for i := 0 to GetArrayLength(aExecArr)-1 do begin 122 | if IsUninstaller() = false then begin 123 | // If app dir already exists while installing, skip add 124 | if (Pos(pathdir[d], aExecArr[i]) > 0) then 125 | updatepath := false; 126 | break; 127 | end else begin 128 | // If app dir exists and = what we originally set, then delete at uninstall 129 | if aExecArr[i] = 'SET PATH=%PATH%;' + pathdir[d] then 130 | aExecArr[i] := ''; 131 | end; 132 | end; 133 | end; 134 | 135 | // If app dir not found, or autoexec.bat didn't exist, then (create and) append to current path 136 | if (IsUninstaller() = false) AND (updatepath = true) then begin 137 | SaveStringToFile(aExecFile, #13#10 + 'SET PATH=%PATH%;' + pathdir[d], True); 138 | 139 | // If uninstalling, write the full autoexec out 140 | end else begin 141 | SaveStringsToFile(aExecFile, aExecArr, False); 142 | end; 143 | end; 144 | end; 145 | end; 146 | 147 | // Split a string into an array using passed delimeter 148 | procedure MPExplode(var Dest: TArrayOfString; Text: String; Separator: String); 149 | var 150 | i: Integer; 151 | begin 152 | i := 0; 153 | repeat 154 | SetArrayLength(Dest, i+1); 155 | if Pos(Separator,Text) > 0 then begin 156 | Dest[i] := Copy(Text, 1, Pos(Separator, Text)-1); 157 | Text := Copy(Text, Pos(Separator,Text) + Length(Separator), Length(Text)); 158 | i := i + 1; 159 | end else begin 160 | Dest[i] := Text; 161 | Text := ''; 162 | end; 163 | until Length(Text)=0; 164 | end; 165 | 166 | 167 | procedure CurStepChanged(CurStep: TSetupStep); 168 | var 169 | taskname: String; 170 | begin 171 | taskname := ModPathName; 172 | if CurStep = ssPostInstall then 173 | if IsTaskSelected(taskname) then 174 | ModPath(); 175 | end; 176 | 177 | procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); 178 | var 179 | aSelectedTasks: TArrayOfString; 180 | i: Integer; 181 | taskname: String; 182 | regpath: String; 183 | regstring: String; 184 | appid: String; 185 | begin 186 | // only run during actual uninstall 187 | if CurUninstallStep = usUninstall then begin 188 | // get list of selected tasks saved in registry at install time 189 | appid := '{#emit SetupSetting("AppId")}'; 190 | if appid = '' then appid := '{#emit SetupSetting("AppName")}'; 191 | regpath := ExpandConstant('Software\Microsoft\Windows\CurrentVersion\Uninstall\'+appid+'_is1'); 192 | RegQueryStringValue(HKLM, regpath, 'Inno Setup: Selected Tasks', regstring); 193 | if regstring = '' then RegQueryStringValue(HKCU, regpath, 'Inno Setup: Selected Tasks', regstring); 194 | 195 | // check each task; if matches modpath taskname, trigger patch removal 196 | if regstring <> '' then begin 197 | taskname := ModPathName; 198 | MPExplode(aSelectedTasks, regstring, ','); 199 | if GetArrayLength(aSelectedTasks) > 0 then begin 200 | for i := 0 to GetArrayLength(aSelectedTasks)-1 do begin 201 | if comparetext(aSelectedTasks[i], taskname) = 0 then 202 | ModPath(); 203 | end; 204 | end; 205 | end; 206 | end; 207 | end; 208 | 209 | function NeedRestart(): Boolean; 210 | var 211 | taskname: String; 212 | begin 213 | taskname := ModPathName; 214 | if IsTaskSelected(taskname) and not UsingWinNT() then begin 215 | Result := True; 216 | end else begin 217 | Result := False; 218 | end; 219 | end; -------------------------------------------------------------------------------- /installer/pandoc-pyplot-setup.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define AppName "pandoc-pyplot" 5 | #define AppVersion "2.3.0.1" 6 | #define AppPublisher "Laurent P. René de Cotret" 7 | #define AppURL "https://github.com/LaurentRDC/pandoc-pyplot" 8 | #define AppEXEName "pandoc-pyplot.exe" 9 | 10 | [Setup] 11 | ; NOTE: The value of AppId uniquely identifies this application. 12 | ; Do not use the same AppId value in installers for other applications. 13 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 14 | AppId={{36E6FD0E-1B80-451E-8B5F-6158004844EE} 15 | AppName={#AppName} 16 | AppVersion={#AppVersion} 17 | AppVerName={#AppName} {#AppVersion} 18 | AppPublisher={#AppPublisher} 19 | AppPublisherURL={#AppURL} 20 | AppSupportURL={#AppURL} 21 | AppUpdatesURL={#AppURL} 22 | DefaultDirName={pf}\{#AppName} 23 | DisableProgramGroupPage=yes 24 | ChangesEnvironment=true 25 | LicenseFile=..\LICENSE.md 26 | OutputDir=.\setup-{#AppVersion} 27 | OutputBaseFilename={#AppName}-installer-{#AppVersion} 28 | Compression=lzma2/ultra64 29 | SolidCompression=yes 30 | 31 | [Languages] 32 | Name: "english"; MessagesFile: "compiler:Default.isl" 33 | 34 | [Files] 35 | Source: ".\installer\pandoc-pyplot.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs 36 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 37 | 38 | [Tasks] 39 | Name: "modifypath"; Description: "Add pandoc-pyplot executable to path"; Flags: checkedonce 40 | 41 | [Run] 42 | Filename: "{app}\{#AppEXEName}"; Description: "{cm:LaunchProgram,{#StringChange(AppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent 43 | 44 | [Code] 45 | const 46 | ModPathName = 'modifypath'; 47 | ModPathType = 'user'; 48 | 49 | function ModPathDir(): TArrayOfString; 50 | begin 51 | setArrayLength(Result, 1) 52 | Result[0] := ExpandConstant('{app}'); 53 | end; 54 | #include "modpath.iss" 55 | -------------------------------------------------------------------------------- /pandoc-pyplot.cabal: -------------------------------------------------------------------------------- 1 | name: pandoc-pyplot 2 | version: 2.3.0.1 3 | cabal-version: >= 1.12 4 | synopsis: A Pandoc filter to include figures generated from Python code blocks 5 | 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. 6 | category: Documentation 7 | homepage: https://github.com/LaurentRDC/pandoc-pyplot#readme 8 | bug-reports: https://github.com/LaurentRDC/pandoc-pyplot/issues 9 | author: Laurent P. René de Cotret 10 | maintainer: Laurent P. René de Cotret 11 | license: GPL-2 12 | license-file: LICENSE.md 13 | build-type: Simple 14 | extra-source-files: 15 | CHANGELOG.md 16 | LICENSE.md 17 | README.md 18 | stack.yaml 19 | test/fixtures/*.py 20 | 21 | source-repository head 22 | type: git 23 | location: https://github.com/LaurentRDC/pandoc-pyplot 24 | 25 | library 26 | exposed-modules: 27 | Text.Pandoc.Filter.Pyplot 28 | Text.Pandoc.Filter.Pyplot.Internal 29 | other-modules: 30 | Paths_pandoc_pyplot 31 | Text.Pandoc.Filter.Pyplot.Configuration 32 | Text.Pandoc.Filter.Pyplot.FigureSpec 33 | Text.Pandoc.Filter.Pyplot.Scripting 34 | Text.Pandoc.Filter.Pyplot.Types 35 | hs-source-dirs: 36 | src 37 | ghc-options: -Wall -Wcompat 38 | build-depends: 39 | base >=4 && <5 40 | , containers 41 | , directory 42 | , data-default-class >= 0.1.2 43 | , filepath >= 1.4 && < 2 44 | , hashable >= 1 && < 2 45 | , pandoc >= 2.8 && <3 46 | , pandoc-types >= 1.20 && < 1.30 47 | , shakespeare >= 2.0 && < 3 48 | , temporary 49 | , text >= 1 && < 2 50 | , typed-process >= 0.2.1 && < 1 51 | , yaml >= 0.8 && < 1 52 | , mtl >= 2.2 && < 2.3 53 | default-language: Haskell2010 54 | 55 | executable pandoc-pyplot 56 | main-is: Main.hs 57 | other-modules: 58 | ManPage 59 | Paths_pandoc_pyplot 60 | hs-source-dirs: 61 | executable 62 | ghc-options: -Wall -Wcompat -rtsopts -threaded -with-rtsopts=-N 63 | build-depends: 64 | base >=4 && <5 65 | , directory 66 | , data-default-class >= 0.1.2 67 | , deepseq 68 | , filepath 69 | , open-browser >= 0.2.1.0 70 | , optparse-applicative >= 0.14 && < 1 71 | , pandoc 72 | , pandoc-pyplot 73 | , pandoc-types 74 | , template-haskell > 2.7 && < 3 75 | , temporary 76 | , text 77 | default-language: Haskell2010 78 | 79 | test-suite tests 80 | type: exitcode-stdio-1.0 81 | hs-source-dirs: test 82 | main-is: Main.hs 83 | build-depends: base >= 4 && < 5 84 | , directory 85 | , data-default-class >= 0.1.2 86 | , filepath 87 | , hspec 88 | , hspec-expectations 89 | , pandoc-types >= 1.12 && <= 2 90 | , pandoc-pyplot 91 | , tasty 92 | , tasty-hunit 93 | , tasty-hspec 94 | , temporary 95 | , text 96 | , mtl >= 2.2 && < 2.3 97 | default-language: Haskell2010 98 | 99 | -------------------------------------------------------------------------------- /src/Text/Pandoc/Filter/Pyplot.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE MultiWayIf #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | {-| 5 | Module : $header$ 6 | Description : Pandoc filter to create Matplotlib/Plotly figures from code blocks 7 | Copyright : (c) Laurent P René de Cotret, 2019 8 | License : GNU GPL, version 2 or above 9 | Maintainer : laurent.decotret@outlook.com 10 | Stability : stable 11 | Portability : portable 12 | 13 | This module defines a Pandoc filter @makePlot@ and related functions 14 | that can be used to walk over a Pandoc document and generate figures from 15 | Python code blocks. 16 | 17 | The syntax for code blocks is simple, Code blocks with the @.pyplot@ or @.plotly@ 18 | attribute will trigger the filter. The code block will be reworked into a Python 19 | script and the output figure will be captured, along with a high-resolution version 20 | of the figure and the source code used to generate the figure. 21 | 22 | To trigger pandoc-pyplot, one of the following is __required__: 23 | 24 | * @.pyplot@: Trigger pandoc-pyplot, rendering via the Matplotlib library 25 | * @.plotly@: Trigger pandoc-pyplot, rendering via the Plotly library 26 | 27 | Here are the possible attributes what pandoc-pyplot understands: 28 | 29 | * @directory=...@ : Directory where to save the figure. 30 | * @format=...@: Format of the generated figure. This can be an extension or an acronym, e.g. @format=png@. 31 | * @caption="..."@: Specify a plot caption (or alternate text). Captions support Markdown formatting and LaTeX math (@$...$@). 32 | * @dpi=...@: Specify a value for figure resolution, or dots-per-inch. Default is 80DPI. (Matplotlib only, ignored otherwise) 33 | * @include=...@: Path to a Python script to include before the code block. Ideal to avoid repetition over many figures. 34 | * @links=true|false@: Add links to source code and high-resolution version of this figure. 35 | This is @true@ by default, but you may wish to disable this for PDF output. 36 | 37 | Custom configurations are possible via the @Configuration@ type and the filter 38 | functions @plotTransformWithConfig@ and @makePlotWithConfig@. 39 | -} 40 | module Text.Pandoc.Filter.Pyplot ( 41 | -- * Operating on single Pandoc blocks 42 | makePlot 43 | , makePlotWithConfig 44 | -- * Operating on whole Pandoc documents 45 | , plotTransform 46 | , plotTransformWithConfig 47 | -- * For configuration purposes 48 | , configuration 49 | , Configuration (..) 50 | , PythonScript 51 | , SaveFormat (..) 52 | -- * For testing and internal purposes only 53 | , PandocPyplotError(..) 54 | , makePlot' 55 | ) where 56 | 57 | import Control.Monad.Reader 58 | 59 | import Data.Default.Class (def) 60 | 61 | import Text.Pandoc.Definition 62 | import Text.Pandoc.Walk (walkM) 63 | 64 | import Text.Pandoc.Filter.Pyplot.Internal 65 | 66 | -- | Main routine to include plots. 67 | -- Code blocks containing the attributes @.pyplot@ or @.plotly@ are considered 68 | -- Python plotting scripts. All other possible blocks are ignored. 69 | makePlot' :: Block -> PyplotM (Either PandocPyplotError Block) 70 | makePlot' block = do 71 | parsed <- parseFigureSpec block 72 | maybe 73 | (return $ Right block) 74 | (\s -> handleResult s <$> runScriptIfNecessary s) 75 | parsed 76 | where 77 | handleResult _ (ScriptChecksFailed msg) = Left $ ScriptChecksFailedError msg 78 | handleResult _ (ScriptFailure code) = Left $ ScriptError code 79 | handleResult spec ScriptSuccess = Right $ toImage spec 80 | 81 | -- | Highest-level function that can be walked over a Pandoc tree. 82 | -- All code blocks that have the @.pyplot@ / @.plotly@ class will be considered 83 | -- figures. 84 | makePlot :: Block -> IO Block 85 | makePlot = makePlotWithConfig def 86 | 87 | -- | like @makePlot@ with with a custom default values. 88 | -- 89 | -- @since 2.1.0.0 90 | makePlotWithConfig :: Configuration -> Block -> IO Block 91 | makePlotWithConfig config block = 92 | runReaderT (makePlot' block >>= either (fail . show) return) config 93 | 94 | -- | Walk over an entire Pandoc document, changing appropriate code blocks 95 | -- into figures. Default configuration is used. 96 | plotTransform :: Pandoc -> IO Pandoc 97 | plotTransform = walkM makePlot 98 | 99 | -- | Walk over an entire Pandoc document, changing appropriate code blocks 100 | -- into figures. The default values are determined by a @Configuration@. 101 | -- 102 | -- @since 2.1.0.0 103 | plotTransformWithConfig :: Configuration -> Pandoc -> IO Pandoc 104 | plotTransformWithConfig = walkM . makePlotWithConfig 105 | -------------------------------------------------------------------------------- /src/Text/Pandoc/Filter/Pyplot/Configuration.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-| 3 | Module : $header$ 4 | Copyright : (c) Laurent P René de Cotret, 2019 5 | License : GNU GPL, version 2 or above 6 | Maintainer : laurent.decotret@outlook.com 7 | Stability : internal 8 | Portability : portable 9 | 10 | Configuration for pandoc-pyplot 11 | -} 12 | 13 | module Text.Pandoc.Filter.Pyplot.Configuration ( 14 | configuration 15 | -- * For testing and internal purposes only 16 | , writeConfig 17 | , inclusionKeys 18 | , directoryKey 19 | , captionKey 20 | , dpiKey 21 | , includePathKey 22 | , saveFormatKey 23 | , withLinksKey 24 | , isTightBboxKey 25 | , isTransparentKey 26 | ) where 27 | 28 | import Data.Default.Class (def) 29 | import Data.Maybe (fromMaybe) 30 | import Data.String (fromString) 31 | import qualified Data.Text.IO as TIO 32 | import Data.Yaml 33 | import Data.Yaml.Config (ignoreEnv, loadYamlSettings) 34 | 35 | import System.Directory (doesFileExist) 36 | 37 | import Text.Pandoc.Filter.Pyplot.Types 38 | 39 | -- | A @Configuration@ cannot be directly created from a YAML file 40 | -- for two reasons: 41 | -- 42 | -- * we want to store an include script. However, it makes more sense to 43 | -- specify the script path in a YAML file. 44 | -- * Save format is best specified by a string, and this must be parsed later 45 | -- 46 | -- Therefore, we have another type, ConfigPrecursor, which CAN be created directly from 47 | -- a YAML file. 48 | data ConfigPrecursor 49 | = ConfigPrecursor 50 | { defaultDirectory_ :: FilePath 51 | , defaultIncludePath_ :: Maybe FilePath 52 | , defaultWithLinks_ :: Bool 53 | , defaultSaveFormat_ :: String 54 | , defaultDPI_ :: Int 55 | , tightBbox_ :: Bool 56 | , transparent_ :: Bool 57 | , interpreter_ :: String 58 | , flags_ :: [String] 59 | } 60 | 61 | instance FromJSON ConfigPrecursor where 62 | parseJSON (Object v) = 63 | ConfigPrecursor 64 | <$> v .:? directoryKey .!= (defaultDirectory def) 65 | <*> v .:? includePathKey 66 | <*> v .:? withLinksKey .!= (defaultWithLinks def) 67 | <*> v .:? saveFormatKey .!= (show $ defaultSaveFormat def) 68 | <*> v .:? dpiKey .!= (defaultDPI def) 69 | <*> v .:? isTightBboxKey .!= (isTightBbox def) 70 | <*> v .:? isTransparentKey .!= (isTransparent def) 71 | <*> v .:? "interpreter" .!= (interpreter def) 72 | <*> v .:? "flags" .!= (flags def) 73 | 74 | parseJSON _ = fail "Could not parse the configuration" 75 | 76 | 77 | renderConfiguration :: ConfigPrecursor -> IO Configuration 78 | renderConfiguration prec = do 79 | includeScript <- fromMaybe mempty $ TIO.readFile <$> defaultIncludePath_ prec 80 | let saveFormat' = fromString $ defaultSaveFormat_ prec 81 | return $ Configuration 82 | { defaultDirectory = defaultDirectory_ prec 83 | , defaultIncludeScript = includeScript 84 | , defaultSaveFormat = saveFormat' 85 | , defaultWithLinks = defaultWithLinks_ prec 86 | , defaultDPI = defaultDPI_ prec 87 | , isTightBbox = tightBbox_ prec 88 | , isTransparent = transparent_ prec 89 | , interpreter = interpreter_ prec 90 | , flags = flags_ prec 91 | } 92 | 93 | 94 | -- | Building configuration from a YAML file. The 95 | -- keys are exactly the same as for Markdown code blocks. 96 | -- 97 | -- If a key is either not present or unreadable, its value will be set 98 | -- to the default value. 99 | -- 100 | -- @since 2.1.0.0 101 | configuration :: FilePath -> IO Configuration 102 | configuration fp = loadYamlSettings [fp] [] ignoreEnv >>= renderConfiguration 103 | 104 | 105 | -- | Write a configuration to file. An exception will be raised in case the file would be overwritten. 106 | -- 107 | -- @since 2.1.3.0 108 | writeConfig :: FilePath -> Configuration -> IO () 109 | writeConfig fp config = do 110 | fileExists <- doesFileExist fp 111 | if fileExists 112 | then error $ mconcat ["File ", fp, " already exists."] 113 | else encodeFile fp config 114 | -------------------------------------------------------------------------------- /src/Text/Pandoc/Filter/Pyplot/FigureSpec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE QuasiQuotes #-} 3 | {-# LANGUAGE TemplateHaskell #-} 4 | {-| 5 | Module : $header$ 6 | Copyright : (c) Laurent P René de Cotret, 2019 7 | License : GNU GPL, version 2 or above 8 | Maintainer : laurent.decotret@outlook.com 9 | Stability : internal 10 | Portability : portable 11 | 12 | This module defines types and functions that help 13 | with keeping track of figure specifications 14 | -} 15 | module Text.Pandoc.Filter.Pyplot.FigureSpec 16 | ( FigureSpec(..) 17 | , SaveFormat(..) 18 | , toImage 19 | , sourceCodePath 20 | , figurePath 21 | , addPlotCapture 22 | , parseFigureSpec 23 | -- for testing purposes 24 | , extension 25 | ) where 26 | 27 | import Control.Monad (join) 28 | import Control.Monad.IO.Class (liftIO) 29 | import Control.Monad.Reader (ask) 30 | 31 | import Data.Default.Class (def) 32 | import Data.Hashable (hash) 33 | import Data.List (intersperse) 34 | import qualified Data.Map.Strict as Map 35 | import Data.Maybe (fromMaybe) 36 | import Data.Monoid ((<>)) 37 | import Data.String (fromString) 38 | import Data.Text (Text, pack, unpack) 39 | import qualified Data.Text.IO as TIO 40 | import Data.Version (showVersion) 41 | 42 | import Paths_pandoc_pyplot (version) 43 | 44 | import System.FilePath (FilePath, addExtension, 45 | makeValid, normalise, 46 | replaceExtension, ()) 47 | 48 | import Text.Pandoc.Builder (fromList, imageWith, link, 49 | para, toList) 50 | import Text.Pandoc.Definition (Block (..), Inline, 51 | Pandoc (..)) 52 | import Text.Shakespeare.Text (st) 53 | 54 | import Text.Pandoc.Class (runPure) 55 | import Text.Pandoc.Extensions (Extension (..), 56 | extensionsFromList) 57 | import Text.Pandoc.Options (ReaderOptions (..)) 58 | import Text.Pandoc.Readers (readMarkdown) 59 | 60 | import Text.Pandoc.Filter.Pyplot.Types 61 | 62 | 63 | -- | Determine inclusion specifications from @Block@ attributes. 64 | -- Note that the @".pyplot"@ OR @.plotly@ class is required, but all other 65 | -- parameters are optional. 66 | parseFigureSpec :: Block -> PyplotM (Maybe FigureSpec) 67 | parseFigureSpec (CodeBlock (id', cls, attrs) content) 68 | | "pyplot" `elem` cls = Just <$> figureSpec Matplotlib 69 | | "plotly" `elem` cls = Just <$> figureSpec Plotly 70 | | otherwise = return Nothing 71 | where 72 | attrs' = Map.fromList attrs 73 | filteredAttrs = filter (\(k, _) -> k `notElem` inclusionKeys) attrs 74 | includePath = unpack <$> Map.lookup includePathKey attrs' 75 | header = "# Generated by pandoc-pyplot " <> ((pack . showVersion) version) 76 | 77 | figureSpec :: RenderingLibrary -> PyplotM FigureSpec 78 | figureSpec lib = do 79 | config <- ask 80 | includeScript <- fromMaybe 81 | (return $ defaultIncludeScript config) 82 | ((liftIO . TIO.readFile) <$> includePath) 83 | return $ 84 | FigureSpec 85 | { caption = Map.findWithDefault mempty captionKey attrs' 86 | , withLinks = fromMaybe (defaultWithLinks config) $ readBool <$> Map.lookup withLinksKey attrs' 87 | , script = mconcat $ intersperse "\n" [header, includeScript, content] 88 | , saveFormat = fromMaybe (defaultSaveFormat config) $ (fromString . unpack) <$> Map.lookup saveFormatKey attrs' 89 | , directory = makeValid $ unpack $ Map.findWithDefault (pack $ defaultDirectory config) directoryKey attrs' 90 | , dpi = fromMaybe (defaultDPI config) $ (read . unpack) <$> Map.lookup dpiKey attrs' 91 | , renderingLib = lib 92 | , tightBbox = isTightBbox config 93 | , transparent = isTransparent config 94 | , blockAttrs = (id', filter (\c -> c `notElem` ["pyplot", "plotly"]) cls, filteredAttrs) 95 | } 96 | 97 | parseFigureSpec _ = return Nothing 98 | 99 | 100 | -- | Convert a @FigureSpec@ to a Pandoc block component. 101 | -- Note that the script to generate figure files must still 102 | -- be run in another function. 103 | toImage :: FigureSpec -> Block 104 | toImage spec = head . toList $ para $ imageWith attrs' (pack target') "fig:" caption' 105 | -- To render images as figures with captions, the target title 106 | -- must be "fig:" 107 | -- Janky? yes 108 | where 109 | attrs' = blockAttrs spec 110 | target' = figurePath spec 111 | withLinks' = withLinks spec 112 | srcLink = link (pack $ replaceExtension target' ".txt") mempty "Source code" 113 | hiresLink = link (pack $ hiresFigurePath spec) mempty "high res." 114 | captionText = fromList $ fromMaybe mempty (captionReader $ caption spec) 115 | captionLinks = mconcat [" (", srcLink, ", ", hiresLink, ")"] 116 | caption' = if withLinks' then captionText <> captionLinks else captionText 117 | 118 | 119 | -- | Determine the path a figure should have. 120 | figurePath :: FigureSpec -> FilePath 121 | figurePath spec = normalise $ directory spec stem spec 122 | where 123 | stem = flip addExtension ext . show . hash 124 | ext = extension . saveFormat $ spec 125 | 126 | 127 | -- | Determine the path to the source code that generated the figure. 128 | sourceCodePath :: FigureSpec -> FilePath 129 | sourceCodePath = normalise . flip replaceExtension ".txt" . figurePath 130 | 131 | 132 | -- | The path to the high-resolution figure. 133 | hiresFigurePath :: FigureSpec -> FilePath 134 | hiresFigurePath spec = normalise $ flip replaceExtension (".hires" <> ext) . figurePath $ spec 135 | where 136 | ext = extension . saveFormat $ spec 137 | 138 | 139 | -- | Modify a Python plotting script to save the figure to a filename. 140 | -- An additional file will also be captured. 141 | addPlotCapture :: FigureSpec -- ^ Path where to save the figure 142 | -> PythonScript -- ^ Code block with added capture 143 | addPlotCapture spec = mconcat 144 | [ script spec <> "\n" 145 | -- Note that the high-resolution figure always has non-transparent background 146 | -- because it is difficult to see the image when opened directly 147 | -- in Chrome, for example. 148 | , plotCapture (renderingLib spec) (figurePath spec) (dpi spec) (transparent spec) (tight') 149 | , plotCapture (renderingLib spec) (hiresFigurePath spec) (minimum [200, 2 * dpi spec]) False (tight') 150 | ] 151 | where 152 | tight' = if tightBbox spec then ("'tight'" :: Text) else ("None" :: Text) 153 | -- Note that, especially for Windows, raw strings (r"...") must be used because path separators might 154 | -- be interpreted as escape characters 155 | plotCapture Matplotlib = captureMatplotlib 156 | plotCapture Plotly = capturePlotly 157 | 158 | 159 | type Tight = Text 160 | type DPI = Int 161 | type IsTransparent = Bool 162 | type RenderingFunc = (FilePath -> DPI -> IsTransparent -> Tight -> PythonScript) 163 | 164 | 165 | -- | Capture plot from Matplotlib 166 | -- Note that, especially for Windows, raw strings (r"...") must be used because path separators might 167 | -- be interpreted as escape characters 168 | captureMatplotlib :: RenderingFunc 169 | captureMatplotlib fname' dpi' transparent' tight' = [st| 170 | import matplotlib.pyplot as plt 171 | plt.savefig(r"#{fname'}", dpi=#{dpi'}, transparent=#{transparent''}, bbox_inches=#{tight'}) 172 | |] 173 | where 174 | transparent'' :: Text 175 | transparent'' = if transparent' then "True" else "False" 176 | 177 | -- | Capture Plotly figure 178 | -- 179 | -- We are trying to emulate the behavior of @matplotlib.pyplot.savefig@ which 180 | -- knows the "current figure". This saves us from contraining users to always 181 | -- have the same Plotly figure name, e.g. @fig@ in all examples on plot.ly 182 | capturePlotly :: RenderingFunc 183 | capturePlotly fname' _ _ _ = [st| 184 | import plotly.graph_objects as go 185 | __current_plotly_figure = next(obj for obj in globals().values() if type(obj) == go.Figure) 186 | __current_plotly_figure.write_image(r"#{fname'}") 187 | |] 188 | 189 | 190 | -- | Reader options for captions. 191 | readerOptions :: ReaderOptions 192 | readerOptions = def 193 | {readerExtensions = 194 | extensionsFromList 195 | [ Ext_tex_math_dollars 196 | , Ext_superscript 197 | , Ext_subscript 198 | , Ext_raw_tex 199 | ] 200 | } 201 | 202 | 203 | -- | Read a figure caption in Markdown format. LaTeX math @$...$@ is supported, 204 | -- as are Markdown subscripts and superscripts. 205 | captionReader :: Text -> Maybe [Inline] 206 | captionReader t = either (const Nothing) (Just . extractFromBlocks) $ runPure $ readMarkdown' t 207 | where 208 | readMarkdown' = readMarkdown readerOptions 209 | 210 | extractFromBlocks (Pandoc _ blocks) = mconcat $ extractInlines <$> blocks 211 | 212 | extractInlines (Plain inlines) = inlines 213 | extractInlines (Para inlines) = inlines 214 | extractInlines (LineBlock multiinlines) = join multiinlines 215 | extractInlines _ = [] 216 | 217 | 218 | -- | Flexible boolean parsing 219 | readBool :: Text -> Bool 220 | readBool s | s `elem` ["True", "true", "'True'", "'true'", "1"] = True 221 | | s `elem` ["False", "false", "'False'", "'false'", "0"] = False 222 | | otherwise = error $ unpack $ mconcat ["Could not parse '", s, "' into a boolean. Please use 'True' or 'False'"] 223 | -------------------------------------------------------------------------------- /src/Text/Pandoc/Filter/Pyplot/Internal.hs: -------------------------------------------------------------------------------- 1 | 2 | {-| 3 | Module : $header$ 4 | Copyright : (c) Laurent P René de Cotret, 2019 5 | License : GNU GPL, version 2 or above 6 | Maintainer : laurent.decotret@outlook.com 7 | Stability : internal 8 | Portability : portable 9 | 10 | This module re-exports internal pandoc-pyplot functionality. 11 | -} 12 | 13 | module Text.Pandoc.Filter.Pyplot.Internal ( 14 | module Text.Pandoc.Filter.Pyplot.Configuration 15 | , module Text.Pandoc.Filter.Pyplot.FigureSpec 16 | , module Text.Pandoc.Filter.Pyplot.Scripting 17 | , module Text.Pandoc.Filter.Pyplot.Types 18 | ) where 19 | 20 | import Text.Pandoc.Filter.Pyplot.Configuration 21 | import Text.Pandoc.Filter.Pyplot.FigureSpec 22 | import Text.Pandoc.Filter.Pyplot.Scripting 23 | import Text.Pandoc.Filter.Pyplot.Types 24 | -------------------------------------------------------------------------------- /src/Text/Pandoc/Filter/Pyplot/Scripting.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | {-| 4 | Module : $header$ 5 | Copyright : (c) Laurent P René de Cotret, 2019 6 | License : GNU GPL, version 2 or above 7 | Maintainer : laurent.decotret@outlook.com 8 | Stability : internal 9 | Portability : portable 10 | 11 | This module defines types and functions that help 12 | with running Python scripts. 13 | -} 14 | module Text.Pandoc.Filter.Pyplot.Scripting 15 | ( runTempPythonScript 16 | , runScriptIfNecessary 17 | ) where 18 | 19 | import Control.Monad.IO.Class 20 | import Control.Monad.Reader.Class 21 | 22 | import Data.Hashable (hash) 23 | import Data.List (intersperse) 24 | import Data.Monoid (Any (..), (<>)) 25 | import qualified Data.Text as T 26 | import qualified Data.Text.IO as T 27 | 28 | import System.Directory (createDirectoryIfMissing, 29 | doesFileExist) 30 | import System.Exit (ExitCode (..)) 31 | import System.FilePath (takeDirectory, ()) 32 | import System.IO.Temp (getCanonicalTemporaryDirectory) 33 | import System.Process.Typed (runProcess, shell) 34 | 35 | import Text.Pandoc.Filter.Pyplot.FigureSpec 36 | import Text.Pandoc.Filter.Pyplot.Types 37 | 38 | -- | Detect the presence of a blocking show call, for example "plt.show()" 39 | checkBlockingShowCall :: PythonScript -> CheckResult 40 | checkBlockingShowCall script' = 41 | if hasShowCall 42 | then CheckFailed "The script has a blocking call to `matplotlib.pyplot.show`. " 43 | else CheckPassed 44 | where 45 | scriptLines = T.lines script' 46 | hasShowCall = getAny $ mconcat $ Any <$> 47 | [ "plt.show()" `elem` scriptLines 48 | , "pyplot.show()" `elem` scriptLines 49 | , "matplotlib.pyplot.show()" `elem` scriptLines 50 | , "fig.show()" `elem` scriptLines 51 | ] 52 | 53 | 54 | -- | List of all script checks 55 | -- This might be overkill right now but extension to other languages will be easier 56 | scriptChecks :: [PythonScript -> CheckResult] 57 | scriptChecks = [checkBlockingShowCall] 58 | 59 | 60 | -- | Take a python script in string form, write it in a temporary directory, 61 | -- then execute it. 62 | runTempPythonScript :: PythonScript -- ^ Content of the script 63 | -> PyplotM ScriptResult -- ^ Result. 64 | runTempPythonScript script' = case checkResult of 65 | CheckFailed msg -> return $ ScriptChecksFailed msg 66 | CheckPassed -> do 67 | -- We involve the script hash as a temporary filename 68 | -- so that there is never any collision 69 | scriptPath <- liftIO $ ( hashedPath) <$> getCanonicalTemporaryDirectory 70 | liftIO $ T.writeFile scriptPath script' 71 | interpreter' <- asks interpreter 72 | flags' <- asks flags 73 | let command = mconcat . intersperse " " $ [interpreter'] <> flags' <> [show scriptPath] 74 | 75 | ec <- liftIO $ runProcess . shell $ command 76 | case ec of 77 | ExitSuccess -> return ScriptSuccess 78 | ExitFailure code -> return $ ScriptFailure code 79 | where 80 | checkResult = mconcat $ scriptChecks <*> [script'] 81 | hashedPath = show . hash $ script' 82 | 83 | 84 | -- | Run the Python script. In case the file already exists, we can safely assume 85 | -- there is no need to re-run it. 86 | runScriptIfNecessary :: FigureSpec 87 | -> PyplotM ScriptResult 88 | runScriptIfNecessary spec = do 89 | liftIO $ createDirectoryIfMissing True . takeDirectory $ figurePath spec 90 | 91 | fileAlreadyExists <- liftIO . doesFileExist $ figurePath spec 92 | result <- if fileAlreadyExists 93 | then return ScriptSuccess 94 | else runTempPythonScript scriptWithCapture 95 | 96 | case result of 97 | ScriptSuccess -> liftIO $ T.writeFile (sourceCodePath spec) (script spec) >> return ScriptSuccess 98 | ScriptFailure code -> return $ ScriptFailure code 99 | ScriptChecksFailed msg -> return $ ScriptChecksFailed msg 100 | 101 | where 102 | scriptWithCapture = addPlotCapture spec 103 | 104 | 105 | -------------------------------------------------------------------------------- /src/Text/Pandoc/Filter/Pyplot/Types.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-| 5 | Module : $header$ 6 | Copyright : (c) Laurent P René de Cotret, 2019 7 | License : GNU GPL, version 2 or above 8 | Maintainer : laurent.decotret@outlook.com 9 | Stability : internal 10 | Portability : portable 11 | 12 | This module defines types in use in pandoc-pyplot 13 | -} 14 | 15 | module Text.Pandoc.Filter.Pyplot.Types where 16 | 17 | import Control.Monad.Reader 18 | 19 | import Data.Char (toLower) 20 | import Data.Default.Class (Default, def) 21 | import Data.Hashable (Hashable) 22 | import Data.List (intersperse) 23 | import Data.Semigroup as Sem 24 | import Data.String (IsString(..)) 25 | import Data.Text (Text) 26 | import Data.Yaml (ToJSON, object, toJSON, (.=)) 27 | 28 | import GHC.Generics (Generic) 29 | 30 | import Text.Pandoc.Definition (Attr) 31 | 32 | 33 | -- | Keys that pandoc-pyplot will look for in code blocks. These are only exported for testing purposes. 34 | directoryKey, captionKey, dpiKey, includePathKey, saveFormatKey, withLinksKey, isTightBboxKey, isTransparentKey :: Text 35 | directoryKey = "directory" 36 | captionKey = "caption" 37 | dpiKey = "dpi" 38 | includePathKey = "include" 39 | saveFormatKey = "format" 40 | withLinksKey = "links" 41 | isTightBboxKey = "tight_bbox" 42 | isTransparentKey = "transparent" 43 | 44 | 45 | -- | list of all keys related to pandoc-pyplot that 46 | -- can be specified in source material. 47 | inclusionKeys :: [Text] 48 | inclusionKeys = [ directoryKey 49 | , captionKey 50 | , dpiKey 51 | , includePathKey 52 | , saveFormatKey 53 | , withLinksKey 54 | , isTightBboxKey 55 | , isTransparentKey 56 | ] 57 | 58 | 59 | -- | Monad in which to run pandoc-pyplot computations 60 | type PyplotM a = ReaderT Configuration IO a 61 | 62 | 63 | -- | String representation of a Python script 64 | type PythonScript = Text 65 | 66 | 67 | -- | Rendering library 68 | -- 69 | -- @since 2.2.0.0 70 | data RenderingLibrary 71 | = Matplotlib -- ^ Rendering via the Matplotlib library. This library has the most features. 72 | | Plotly -- ^ Rendering via the Plotly library. 73 | deriving (Show, Eq, Generic) 74 | 75 | instance Hashable RenderingLibrary 76 | 77 | 78 | -- | Possible result of running a Python script 79 | data ScriptResult 80 | = ScriptSuccess 81 | | ScriptChecksFailed String 82 | | ScriptFailure Int 83 | 84 | 85 | -- | Result of checking scripts for problems 86 | data CheckResult 87 | = CheckPassed 88 | | CheckFailed String 89 | deriving (Eq) 90 | 91 | instance Sem.Semigroup CheckResult where 92 | (<>) CheckPassed a = a 93 | (<>) a CheckPassed = a 94 | (<>) (CheckFailed msg1) (CheckFailed msg2) = CheckFailed (msg1 <> msg2) 95 | 96 | instance Monoid CheckResult where 97 | mempty = CheckPassed 98 | 99 | #if !(MIN_VERSION_base(4,11,0)) 100 | mappend = (<>) 101 | #endif 102 | 103 | 104 | -- | Possible errors returned by the filter 105 | data PandocPyplotError 106 | = ScriptError Int -- ^ Running Python script has yielded an error 107 | | ScriptChecksFailedError String -- ^ Python script did not pass all checks 108 | deriving (Eq) 109 | 110 | instance Show PandocPyplotError where 111 | show (ScriptError exitcode) = "Script error: plot could not be generated. Exit code " <> (show exitcode) 112 | show (ScriptChecksFailedError msg) = "Script did not pass all checks: " <> msg 113 | 114 | 115 | -- | Generated figure file format supported by pandoc-pyplot. 116 | -- Note: all formats are supported by Matplotlib, but not all 117 | -- formats are supported by Plotly 118 | data SaveFormat 119 | = PNG 120 | | PDF 121 | | SVG 122 | | JPG 123 | | EPS 124 | | GIF 125 | | TIF 126 | deriving (Bounded, Enum, Eq, Show, Generic) 127 | 128 | instance IsString SaveFormat where 129 | -- | An error is thrown if the save format cannot be parsed. 130 | fromString s 131 | | s `elem` ["png", "PNG", ".png"] = PNG 132 | | s `elem` ["pdf", "PDF", ".pdf"] = PDF 133 | | s `elem` ["svg", "SVG", ".svg"] = SVG 134 | | s `elem` ["eps", "EPS", ".eps"] = EPS 135 | | s `elem` ["gif", "GIF", ".gif"] = GIF 136 | | s `elem` ["jpg", "jpeg", "JPG", "JPEG", ".jpg", ".jpeg"] = JPG 137 | | s `elem` ["tif", "tiff", "TIF", "TIFF", ".tif", ".tiff"] = TIF 138 | | otherwise = error $ 139 | mconcat [ s 140 | , " is not one of valid save format : " 141 | , mconcat $ intersperse ", " $ show <$> saveFormats 142 | ] 143 | where 144 | saveFormats = (enumFromTo minBound maxBound) :: [SaveFormat] 145 | 146 | instance Hashable SaveFormat -- From Generic 147 | 148 | -- | Save format file extension 149 | extension :: SaveFormat -> String 150 | extension fmt = mconcat [".", fmap toLower . show $ fmt] 151 | 152 | -- | Default interpreter should be Python 3, which has a different 153 | -- name on Windows ("python") vs Unix ("python3") 154 | -- 155 | -- @since 2.1.2.0 156 | defaultPlatformInterpreter :: String 157 | #if defined(mingw32_HOST_OS) 158 | defaultPlatformInterpreter = "python" 159 | #else 160 | defaultPlatformInterpreter = "python3" 161 | #endif 162 | 163 | -- | Configuration of pandoc-pyplot, describing the default behavior 164 | -- of the filter. 165 | -- 166 | -- A Configuration is useful when dealing with lots of figures; it avoids 167 | -- repeating the same values.sta 168 | -- 169 | -- @since 2.1.0.0 170 | data Configuration 171 | = Configuration 172 | { defaultDirectory :: FilePath -- ^ The default directory where figures will be saved. 173 | , defaultIncludeScript :: PythonScript -- ^ The default script to run before other instructions. 174 | , defaultWithLinks :: Bool -- ^ The default behavior of whether or not to include links to source code and high-res 175 | , defaultSaveFormat :: SaveFormat -- ^ The default save format of generated figures. 176 | , defaultDPI :: Int -- ^ The default dots-per-inch value for generated figures. Matplotlib only, ignored otherwise. 177 | , isTightBbox :: Bool -- ^ Whether the figures should be saved with @bbox_inches="tight"@ or not. Useful for larger figures with subplots. Matplotlib only, ignored otherwise. 178 | , isTransparent :: Bool -- ^ If True, figures will be saved with transparent background rather than solid color. .Matplotlib only, ignored otherwise. 179 | , interpreter :: String -- ^ The name of the interpreter to use to render figures. 180 | , flags :: [String] -- ^ Command-line flags to be passed to the Python interpreger, e.g. ["-O", "-Wignore"] 181 | } 182 | deriving (Eq, Show) 183 | 184 | instance Default Configuration where 185 | def = Configuration { 186 | defaultDirectory = "generated/" 187 | , defaultIncludeScript = mempty 188 | , defaultWithLinks = True 189 | , defaultSaveFormat = PNG 190 | , defaultDPI = 80 191 | , isTightBbox = False 192 | , isTransparent = False 193 | , interpreter = defaultPlatformInterpreter 194 | , flags = mempty 195 | } 196 | 197 | instance ToJSON Configuration where 198 | toJSON (Configuration dir' _ withLinks' savefmt' dpi' tightbbox' transparent' interp' flags') = 199 | -- We ignore the include script as we want to examplify that 200 | -- this is for a filepath 201 | object [ directoryKey .= dir' 202 | , includePathKey .= ("example.py" :: FilePath) 203 | , withLinksKey .= withLinks' 204 | , dpiKey .= dpi' 205 | , saveFormatKey .= (fmap toLower . show $ savefmt') 206 | , isTightBboxKey .= tightbbox' 207 | , isTransparentKey .= transparent' 208 | , "interpreter" .= interp' 209 | , "flags" .= flags' 210 | ] 211 | 212 | 213 | -- | Datatype containing all parameters required to run pandoc-pyplot. 214 | -- 215 | -- It is assumed that once a @FigureSpec@ has been created, no configuration 216 | -- can overload it; hence, a @FigureSpec@ completely encodes a particular figure. 217 | data FigureSpec = FigureSpec 218 | { caption :: Text -- ^ Figure caption. 219 | , withLinks :: Bool -- ^ Append links to source code and high-dpi figure to caption. 220 | , script :: PythonScript -- ^ Source code for the figure. 221 | , saveFormat :: SaveFormat -- ^ Save format of the figure. 222 | , directory :: FilePath -- ^ Directory where to save the file. 223 | , dpi :: Int -- ^ Dots-per-inch of figure. 224 | , renderingLib :: RenderingLibrary -- ^ Rendering library. 225 | , tightBbox :: Bool -- ^ Enforce tight bounding-box with @bbox_inches="tight"@. 226 | , transparent :: Bool -- ^ Make figure background transparent. 227 | , blockAttrs :: Attr -- ^ Attributes not related to @pandoc-pyplot@ will be propagated. 228 | } deriving Generic 229 | 230 | instance Hashable FigureSpec -- From Generic 231 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by 'stack init' 2 | # 3 | # Some commonly used options have been documented as comments in this file. 4 | # For advanced use and comprehensive documentation of the format, please see: 5 | # https://docs.haskellstack.org/en/stable/yaml_configuration/ 6 | 7 | # Resolver to choose a 'specific' stackage snapshot or a compiler version. 8 | # A snapshot resolver dictates the compiler version and the set of packages 9 | # to be used for project dependencies. For example: 10 | # 11 | # resolver: lts-3.5 12 | # resolver: nightly-2015-09-21 13 | # resolver: ghc-7.10.2 14 | # 15 | # The location of a snapshot can be provided as a file or url. Stack assumes 16 | # a snapshot provided as a file might change, whereas a url resource does not. 17 | # 18 | # resolver: ./custom-snapshot.yaml 19 | # resolver: https://example.com/snapshots/2018-01-01.yaml 20 | resolver: lts-14.16 21 | 22 | # User packages to be built. 23 | # Various formats can be used as shown in the example below. 24 | # 25 | # packages: 26 | # - some-directory 27 | # - https://example.com/foo/bar/baz-0.0.2.tar.gz 28 | # - location: 29 | # git: https://github.com/commercialhaskell/stack.git 30 | # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a 31 | # - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a 32 | # subdirs: 33 | # - auto-update 34 | # - wai 35 | packages: 36 | - . 37 | # Dependency packages to be pulled from upstream that are not in the resolver 38 | # using the same syntax as the packages field. 39 | # (e.g., acme-missiles-0.3) 40 | extra-deps: 41 | - pandoc-2.8 42 | - pandoc-types-1.20@sha256:8393b1a73b8a6a1f3feaeb3a6592c176461082c3e4d897f1b316b1a58dd84c39,3999 43 | - texmath-0.12 44 | - HsYAML-0.2.1.0@sha256:e4677daeba57f7a1e9a709a1f3022fe937336c91513e893166bd1f023f530d68,5311 45 | - doclayout-0.2.0.1@sha256:0410e40c4fa8e299b4f5fa03d378111b9d0effdf59134c95882a160637887ba8,2063 46 | - doctemplates-0.7.2@sha256:e9a3a2182446f6f4548096d1349088144c349c504675660ab30e432af3136bda,3113 47 | - haddock-library-1.8.0@sha256:9dece2cbca827755fdfc30af5a407b0ca30cf29ec1ee85215b38fd8fc23e7421,3723 48 | - skylighting-0.8.2.3@sha256:8155893fe493dbd64367573f6f87338375f50f7003a8cc7ef8ae3836aea52a29,9730 49 | - skylighting-core-0.8.2.3@sha256:3de402288d8748a334461a73bbbbdc0bc92a8533c983bfaeb7017d4653e283e6,8058 50 | - regex-pcre-builtin-0.95.1.1.8.43@sha256:2d671af361adf1776fde182a687bb6da022b1e5e3b0a064ce264289de63564a5,3088 51 | - regex-base-0.94.0.0@sha256:d514eab2aa9ba4b9d14900ac40cbdea1993372466a6cc6ffeeab59a1975563c0,2166 52 | 53 | # Override default flag values for local packages and extra-deps 54 | # flags: {} 55 | 56 | # Extra package databases containing global packages 57 | # extra-package-dbs: [] 58 | 59 | # Control whether we use the GHC we find on the path 60 | # system-ghc: true 61 | # 62 | # Require a specific version of stack, using version ranges 63 | # require-stack-version: -any # Default 64 | # require-stack-version: ">=1.10" 65 | # 66 | # Override the architecture used by stack, especially useful on Windows 67 | # arch: i386 68 | # arch: x86_64 69 | # 70 | # Extra directories used by stack for building 71 | # extra-include-dirs: [/path/to/dir] 72 | # extra-lib-dirs: [/path/to/dir] 73 | # 74 | # Allow a newer minor version of GHC than the snapshot specifies 75 | # compiler-check: newer-minor 76 | -------------------------------------------------------------------------------- /test/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | module Main where 5 | 6 | import Control.Monad (unless) 7 | import Control.Monad.Reader 8 | 9 | import Data.Default.Class (def) 10 | import Data.List (isInfixOf, isSuffixOf) 11 | import Data.Monoid ((<>)) 12 | import Data.Text (unpack, pack) 13 | 14 | import Test.Tasty 15 | import Test.Tasty.HUnit 16 | 17 | import Text.Pandoc.Filter.Pyplot 18 | import Text.Pandoc.Filter.Pyplot.Internal 19 | 20 | import qualified Text.Pandoc.Builder as B 21 | import qualified Text.Pandoc.Definition as B 22 | import Text.Pandoc.JSON 23 | 24 | import System.Directory (createDirectory, 25 | createDirectoryIfMissing, 26 | doesDirectoryExist, 27 | doesFileExist, 28 | listDirectory, 29 | removeDirectoryRecursive, 30 | removePathForcibly) 31 | import System.FilePath (takeExtensions, ()) 32 | import System.IO.Temp (getCanonicalTemporaryDirectory) 33 | 34 | main :: IO () 35 | main = 36 | defaultMain $ 37 | testGroup 38 | "Text.Pandoc.Filter.Pyplot" 39 | [ testFileCreation 40 | , testFileInclusion 41 | , testSaveFormat 42 | , testBlockingCallError 43 | , testMarkdownFormattingCaption 44 | , testWithLinks 45 | , testWithConfiguration 46 | , testOverridingConfiguration 47 | , testBuildConfiguration 48 | ] 49 | 50 | plotCodeBlock :: PythonScript -> Block 51 | plotCodeBlock script = CodeBlock (mempty, ["pyplot"], mempty) script 52 | 53 | addCaption :: String -> Block -> Block 54 | addCaption caption (CodeBlock (id', cls, attrs) script) = 55 | CodeBlock (id', cls, attrs ++ [(captionKey, pack caption)]) script 56 | 57 | addDirectory :: FilePath -> Block -> Block 58 | addDirectory dir (CodeBlock (id', cls, attrs) script) = 59 | CodeBlock (id', cls, attrs ++ [(directoryKey, pack dir)]) script 60 | 61 | addInclusion :: FilePath -> Block -> Block 62 | addInclusion inclusionPath (CodeBlock (id', cls, attrs) script) = 63 | CodeBlock (id', cls, attrs ++ [(includePathKey, pack inclusionPath)]) script 64 | 65 | addSaveFormat :: SaveFormat -> Block -> Block 66 | addSaveFormat saveFormat (CodeBlock (id', cls, attrs) script) = 67 | CodeBlock (id', cls, attrs ++ [(saveFormatKey, pack . extension $ saveFormat)]) script 68 | 69 | addDPI :: Int -> Block -> Block 70 | addDPI dpi (CodeBlock (id', cls, attrs) script) = 71 | CodeBlock (id', cls, attrs ++ [(dpiKey, pack . show $ dpi)]) script 72 | 73 | addWithLinks :: Bool -> Block -> Block 74 | addWithLinks yn (CodeBlock (id', cls, attrs) script) = 75 | CodeBlock (id', cls, attrs ++ [(withLinksKey, pack . show $ yn)]) script 76 | 77 | 78 | -- | Assert that a file exists 79 | assertFileExists :: HasCallStack => FilePath -> Assertion 80 | assertFileExists filepath = do 81 | fileExists <- doesFileExist filepath 82 | unless fileExists (assertFailure msg) 83 | where 84 | msg = mconcat ["File ", filepath, " does not exist."] 85 | 86 | -- | Not available with GHC < 8.4 87 | -- since this function was added in filepath-1.4.2 88 | -- but GHC 8.2.2 comes with filepath-1.4.1.2 89 | isExtensionOf :: String -> FilePath -> Bool 90 | isExtensionOf ext@('.':_) = isSuffixOf ext . takeExtensions 91 | isExtensionOf ext = isSuffixOf ('.':ext) . takeExtensions 92 | 93 | 94 | -- | Assert that the first list is contained, 95 | -- wholly and intact, anywhere within the second. 96 | assertIsInfix :: (Eq a, Show a, HasCallStack) => [a] -> [a] -> Assertion 97 | assertIsInfix xs ys = unless (xs `isInfixOf` ys) (assertFailure msg) 98 | where 99 | msg = mconcat ["Expected ", show xs, " to be an infix of ", show ys] 100 | 101 | -- Ensure a directory is empty but exists. 102 | ensureDirectoryExistsAndEmpty :: FilePath -> IO () 103 | ensureDirectoryExistsAndEmpty dir = do 104 | exists <- doesDirectoryExist dir 105 | if exists 106 | then removePathForcibly dir 107 | else return () 108 | createDirectory dir 109 | 110 | ------------------------------------------------------------------------------- 111 | -- Test that plot files and source files are created when the filter is run 112 | testFileCreation :: TestTree 113 | testFileCreation = 114 | testCase "writes output files in appropriate directory" $ do 115 | tempDir <- ( "test-file-creation") <$> getCanonicalTemporaryDirectory 116 | ensureDirectoryExistsAndEmpty tempDir 117 | 118 | let codeBlock = (addDirectory tempDir $ plotCodeBlock "import matplotlib.pyplot as plt\n") 119 | _ <- runReaderT (makePlot' codeBlock) def 120 | filesCreated <- length <$> listDirectory tempDir 121 | assertEqual "" filesCreated 3 122 | 123 | ------------------------------------------------------------------------------- 124 | -- Test that included files are found within the source 125 | testFileInclusion :: TestTree 126 | testFileInclusion = 127 | testCase "includes plot inclusions" $ do 128 | tempDir <- ( "test-file-inclusion") <$> getCanonicalTemporaryDirectory 129 | ensureDirectoryExistsAndEmpty tempDir 130 | 131 | let codeBlock = 132 | (addInclusion "test/fixtures/include.py" $ 133 | addDirectory tempDir $ plotCodeBlock "import matplotlib.pyplot as plt\n") 134 | _ <- runReaderT (makePlot' codeBlock) def 135 | inclusion <- readFile "test/fixtures/include.py" 136 | sourcePath <- head . filter (isExtensionOf "txt") <$> listDirectory tempDir 137 | src <- readFile (tempDir sourcePath) 138 | assertIsInfix inclusion src 139 | 140 | ------------------------------------------------------------------------------- 141 | -- Test that the files are saved in the appropriate format 142 | testSaveFormat :: TestTree 143 | testSaveFormat = 144 | testCase "saves in the appropriate format" $ do 145 | tempDir <- ( "test-safe-format") <$> getCanonicalTemporaryDirectory 146 | ensureDirectoryExistsAndEmpty tempDir 147 | 148 | let codeBlock = 149 | (addSaveFormat JPG $ 150 | addDirectory tempDir $ 151 | plotCodeBlock 152 | "import matplotlib.pyplot as plt\nplt.figure()\nplt.plot([1,2], [1,2])") 153 | _ <- runReaderT (makePlot' codeBlock) def 154 | numberjpgFiles <- 155 | length <$> filter (isExtensionOf (extension JPG)) <$> 156 | listDirectory tempDir 157 | assertEqual "" numberjpgFiles 2 158 | 159 | ------------------------------------------------------------------------------- 160 | -- Test that a script containing a blocking call to matplotlib.pyplot.show 161 | -- returns the appropriate error 162 | testBlockingCallError :: TestTree 163 | testBlockingCallError = 164 | testCase "raises an exception for blocking calls" $ do 165 | tempDir <- ( "test-blocking-call-error") <$> getCanonicalTemporaryDirectory 166 | ensureDirectoryExistsAndEmpty tempDir 167 | 168 | let codeBlock = addDirectory tempDir $ plotCodeBlock "import matplotlib.pyplot as plt\nplt.show()" 169 | result <- runReaderT (makePlot' codeBlock) def 170 | case result of 171 | Right block -> assertFailure "did not catch the expected blocking call" 172 | Left error -> 173 | if isCheckError error 174 | then pure () 175 | else assertFailure "An error was caught but not the expected blocking call" 176 | where 177 | isCheckError (ScriptChecksFailedError msg) = True 178 | isCheckError _ = False 179 | ------------------------------------------------------------------------------- 180 | 181 | ------------------------------------------------------------------------------- 182 | -- Test that Markdown formatting in captions is correctly rendered 183 | testMarkdownFormattingCaption :: TestTree 184 | testMarkdownFormattingCaption = 185 | testCase "appropriately parses Markdown captions" $ do 186 | tempDir <- ( "test-caption-parsing") <$> getCanonicalTemporaryDirectory 187 | ensureDirectoryExistsAndEmpty tempDir 188 | 189 | -- Note that this test is fragile, in the sense that the expected result must be carefully 190 | -- constructed 191 | let expected = [B.Strong [B.Str "caption"]] 192 | codeBlock = addDirectory tempDir $ addCaption "**caption**" $ plotCodeBlock "import matplotlib.pyplot as plt" 193 | result <- runReaderT (makePlot' codeBlock) def 194 | case result of 195 | Left error -> assertFailure $ "an error occured: " <> show error 196 | Right block -> assertIsInfix expected (extractCaption block) 197 | where 198 | extractCaption (B.Para blocks) = extractImageCaption . head $ blocks 199 | extractCaption _ = mempty 200 | 201 | extractImageCaption (Image _ c _) = c 202 | extractImageCaption _ = mempty 203 | ------------------------------------------------------------------------------- 204 | 205 | ------------------------------------------------------------------------------- 206 | -- Test that it is possible to not render links in captions 207 | testWithLinks :: TestTree 208 | testWithLinks = 209 | testCase "appropriately omits links to source code and high-res image" $ do 210 | tempDir <- ( "test-caption-links") <$> getCanonicalTemporaryDirectory 211 | ensureDirectoryExistsAndEmpty tempDir 212 | 213 | -- Note that this test is fragile, in the sense that the expected result must be carefully 214 | -- constructed 215 | let expected = mempty 216 | codeBlock = addWithLinks False $ addDirectory tempDir $ addCaption mempty $ plotCodeBlock "import matplotlib.pyplot as plt" 217 | result <- runReaderT (makePlot' codeBlock) def 218 | case result of 219 | Left error -> assertFailure $ "an error occured: " <> show error 220 | Right block -> assertIsInfix expected (extractCaption block) 221 | where 222 | extractCaption (B.Para blocks) = extractImageCaption . head $ blocks 223 | extractCaption _ = mempty 224 | 225 | extractImageCaption (Image _ c _) = c 226 | extractImageCaption _ = mempty 227 | ------------------------------------------------------------------------------- 228 | 229 | 230 | ------------------------------------------------------------------------------- 231 | -- Test with configuration 232 | testConfig :: IO Configuration 233 | testConfig = do 234 | tempDir <- ( "test-with-config") <$> getCanonicalTemporaryDirectory 235 | ensureDirectoryExistsAndEmpty tempDir 236 | 237 | return $ def {defaultDirectory = tempDir, defaultSaveFormat = JPG} 238 | 239 | testOverridingConfiguration :: TestTree 240 | testOverridingConfiguration = 241 | testCase "follows the configuration options" $ do 242 | config <- testConfig 243 | 244 | -- The default from config says the save format should be JPG 245 | -- but the code block save format="png" 246 | let codeBlock = (addSaveFormat PNG $ 247 | plotCodeBlock 248 | "import matplotlib.pyplot as plt\nplt.figure()\nplt.plot([1,2], [1,2])") 249 | _ <- runReaderT (makePlot' codeBlock) config 250 | 251 | numberjpgFiles <- 252 | length <$> filter (isExtensionOf (extension JPG)) <$> 253 | listDirectory (defaultDirectory config) 254 | numberpngFiles <- 255 | length <$> filter (isExtensionOf (extension PNG)) <$> 256 | listDirectory (defaultDirectory config) 257 | assertEqual "" numberjpgFiles 0 258 | assertEqual "" numberpngFiles 2 259 | ------------------------------------------------------------------------------- 260 | 261 | ------------------------------------------------------------------------------- 262 | -- Test that values in code blocks will override the defaults in configuration 263 | testWithConfiguration :: TestTree 264 | testWithConfiguration = 265 | testCase "code block attributes override configuration defaults" $ do 266 | config <- testConfig 267 | 268 | let codeBlock = plotCodeBlock "import matplotlib.pyplot as plt\nplt.figure()\nplt.plot([1,2], [1,2])" 269 | _ <- runReaderT (makePlot' codeBlock) config 270 | 271 | numberjpgFiles <- 272 | length <$> filter (isExtensionOf (extension JPG)) <$> 273 | listDirectory (defaultDirectory config) 274 | assertEqual "" numberjpgFiles 2 275 | ------------------------------------------------------------------------------- 276 | 277 | testBuildConfiguration :: TestTree 278 | testBuildConfiguration = 279 | testCase "configuration is correctly parsed" $ do 280 | let config = def { defaultDirectory = "generated/other" 281 | , defaultSaveFormat = JPG 282 | , defaultDPI = 150 283 | , flags = ["-Wignore"] 284 | , isTightBbox = True 285 | , isTransparent = True 286 | } 287 | parsedConfig <- configuration "test/fixtures/.pandoc-pyplot.yml" 288 | assertEqual "" config parsedConfig 289 | -------------------------------------------------------------------------------- /test/fixtures/.pandoc-pyplot.yml: -------------------------------------------------------------------------------- 1 | directory: generated/other 2 | format: jpg 3 | dpi: 150 4 | flags: [-Wignore] 5 | transparent: true 6 | tight_bbox: true -------------------------------------------------------------------------------- /test/fixtures/include.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | plt.style.use('ggplot') 3 | -------------------------------------------------------------------------------- /test/fixtures/integration.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: test of pandoc-pyplot 3 | --- 4 | 5 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 6 | 7 | ```{.pyplot} 8 | import matplotlib.pyplot as plt 9 | import numpy as np 10 | 11 | x = np.linspace(0, 10, 1024) 12 | y = 10*np.sin(2*np.pi*x) 13 | n = np.random.random(size = x.shape) - 0.5 14 | 15 | # First create an empty figure 16 | plt.figure() 17 | 18 | # Plot both noisy signal and expected sinusoid 19 | plt.plot(x, y + n, 'r.') 20 | plt.plot(x, y, 'k-') 21 | 22 | # Plot formatting 23 | plt.xlabel('Abcissa') 24 | plt.ylabel('Ordinates') 25 | plt.title('Sinusoid') 26 | ``` 27 | 28 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 29 | 30 | ```{.pyplot caption="This is a **caption** with *Markdown* formatting. It also includes math symbols like $\alpha$ and $\beta$"} 31 | import matplotlib.pyplot as plt 32 | import numpy as np 33 | 34 | x = np.linspace(0, 10, 1024) 35 | y = 10*np.sin(2*np.pi*x) 36 | n = np.random.random(size = x.shape) - 0.5 37 | 38 | # First create an empty figure 39 | fig, (ax1, ax2) = plt.subplots(nrows=2, ncols=1, sharex=True) 40 | 41 | # Plot both noisy signal and expected sinusoid 42 | ax1.plot(x, y + n, 'g.') 43 | ax1.plot(x, y, 'k-') 44 | 45 | # Residuals 46 | ax2.plot(x, n, '.g') 47 | ax2.axhline(y=0, color='k') 48 | 49 | # Plot formatting 50 | ax2.set_xlabel('Abcissa') 51 | ax1.set_title('Residuals') 52 | ``` 53 | 54 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 55 | 56 | ```{.pyplot caption="This plot has a different style" links=False} 57 | import numpy as np 58 | import matplotlib.pyplot as plt 59 | 60 | x = np.linspace(0, 10, 1024) 61 | y = 10*np.cos(np.pi*x) 62 | n = (np.random.random(size = x.shape) - 0.5) * 5 63 | 64 | # First create an empty figure 65 | fig, (ax1, ax2) = plt.subplots(nrows=2, ncols=1, sharex=True) 66 | 67 | # Plot both noisy signal and expected sinusoid 68 | ax1.plot(x, y + n, 'b.') 69 | ax1.plot(x, y, 'k-') 70 | 71 | # Residuals 72 | ax2.plot(x, n, '.b') 73 | ax2.axhline(y=0, color='k') 74 | 75 | # Plot formatting 76 | ax2.set_xlabel('Abcissa') 77 | ax1.set_title('Residuals in ggplot style') 78 | ``` 79 | 80 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 81 | 82 | ```{.plotly caption="This is a Plotly figure!"} 83 | import plotly.graph_objects as go 84 | import numpy as np 85 | np.random.seed(1) 86 | 87 | N = 100 88 | x = np.random.rand(N) 89 | y = np.random.rand(N) 90 | colors = np.random.rand(N) 91 | sz = np.random.rand(N) * 30 92 | 93 | fig = go.Figure() 94 | fig.add_trace(go.Scatter( 95 | x=x, 96 | y=y, 97 | mode="markers", 98 | marker=go.scatter.Marker( 99 | size=sz, 100 | color=colors, 101 | opacity=0.6, 102 | colorscale="Viridis" 103 | ) 104 | )) 105 | ``` --------------------------------------------------------------------------------