├── .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 | [](http://hackage.haskell.org/package/pandoc-pyplot) [](http://stackage.org/nightly/package/pandoc-pyplot) [](https://ci.appveyor.com/project/LaurentRDC/pandoc-pyplot) [](https://dev.azure.com/laurentdecotret/pandoc-pyplot/_build/latest?definitionId=2&branchName=master)
6 | 
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 | ```
--------------------------------------------------------------------------------