packages feed

pandoc-plot (empty) → 0.1.0.0

raw patch · 25 files changed

+2620/−0 lines, 25 filesdep +basedep +containersdep +data-default-classsetup-changed

Dependencies added: base, containers, data-default-class, deepseq, directory, filepath, hashable, hspec, hspec-expectations, mtl, open-browser, optparse-applicative, pandoc, pandoc-plot, pandoc-types, shakespeare, tasty, tasty-hspec, tasty-hunit, template-haskell, temporary, text, typed-process, yaml

Files

+ CHANGELOG.md view
@@ -0,0 +1,8 @@+# Change log
+
+pandoc-plot uses [Semantic Versioning](http://semver.org/spec/v2.0.0.html)
+
+Release 0.1.0.0
+---------------
+
+* Initial release
+ LICENSE view
@@ -0,0 +1,339 @@+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
+ README.md view
@@ -0,0 +1,300 @@+# pandoc-plot 
+
+## A Pandoc filter to generate figures from code blocks in documents using your plotting toolkit of choice
+
+[![Build status](https://ci.appveyor.com/api/projects/status/mmgiuk52j356e6jp?svg=true)](https://ci.appveyor.com/project/LaurentRDC/pandoc-plot) [![Build Status](https://dev.azure.com/laurentdecotret/pandoc-plot/_apis/build/status/LaurentRDC.pandoc-plot?branchName=master)](https://dev.azure.com/laurentdecotret/pandoc-plot/_build/latest?definitionId=5&branchName=master) ![GitHub](https://img.shields.io/github/license/LaurentRDC/pandoc-plot)
+
+`pandoc-plot` turns code blocks present in your documents into embedded figures, using your plotting toolkit of choice, including Matplotlib, ggplot2, MATLAB, Mathematica, and more.
+
+## Table of content
+
+* [Usage](#usage)
+* [Supported toolkits](#supported-toolkits)
+* [Features](#features)
+    * [Captions](#captions)
+    * [Link to source code](#link-to-source-code)
+    * [Preamble scripts](#preamble-scripts)
+    * [No wasted work](#no-wasted-work)
+    * [Compatibility with pandoc-crossref](#compatibility-with-pandoc-crossref)
+* [Configuration](#configuration)
+    * [Toolkit-specific options](#toolkit-specific-options)
+* [Installation](#installation)
+* [Warning](#warning)
+
+## Usage
+
+This program is a [Pandoc](https://pandoc.org/) filter. It operates on the Pandoc abstract syntax tree, and can therefore be used in the middle of conversion from input format to output format.
+
+The filter recognizes code blocks with classes that match plotting toolkits. For example, using the `matplotlib` toolkit:
+
+~~~markdown
+# My document
+
+This is a paragraph.
+
+```{.matplotlib}
+import matplotlib.pyplot as plt
+
+plt.figure()
+plt.plot([0,1,2,3,4], [1,2,3,4,5])
+plt.title('This is an example figure')
+```
+~~~
+
+Putting the above in `input.md`, we can then generate the plot and embed it in an HTML page:
+
+```bash
+pandoc --filter pandoc-plot input.md --output output.html
+```
+
+## Supported toolkits
+
+`pandoc-plot` currently supports the following plotting toolkits (installed separately):
+
+* `matplotlib`: plots using the [matplotlib](https://matplotlib.org/) Python library;
+* `plotly_python` : plots using the [plotly](https://plot.ly/python/) Python library;
+* `matlabplot`: plots using [MATLAB](https://www.mathworks.com/);
+* `mathplot` : plots using [Mathematica](https://www.wolfram.com/mathematica/);
+* `octaveplot`: plots using [GNU Octave](https://www.gnu.org/software/octave/);
+* `ggplot2`: plots using [ggplot2](https://ggplot2.tidyverse.org/);
+
+To know which toolkits are useable on *your machine* (and which ones are not available), you can check with the `--toolkits/-t` flag:
+
+```bash
+pandoc-plot --toolkits
+```
+
+### In progress
+
+Support for the following plotting toolkits is coming:
+
+* [gnuplot](http://www.gnuplot.info/)
+* [Plotly R](https://plot.ly/r/)
+
+**Wish your plotting toolkit of choice was available? Please [raise an issue](https://github.com/LaurentRDC/pandoc-plot/issues)!**
+
+## Features
+
+### Captions
+
+You can also specify a caption for your image. This is done using the optional `caption` parameter.
+
+__Markdown__:
+
+~~~markdown
+```{.matlabplot caption="This is a simple figure"}
+x  = 0: .1 : 2*pi;
+y1 = cos(x);
+y2 = sin(x);
+
+figure
+plot(x, y1, 'b', x, y2, 'r-.', 'LineWidth', 2)
+```
+~~~
+
+__LaTex__:
+
+```latex
+\begin{minted}[caption=This is a simple figure]{matlabplot}
+x  = 0: .1 : 2*pi;
+y1 = cos(x);
+y2 = sin(x);
+
+figure
+plot(x, y1, 'b', x, y2, 'r-.', 'LineWidth', 2)
+\end{minted}
+```
+
+Caption formatting is either plain text or Markdown. LaTeX-style math is also support in captions (using dollar signs $...$).
+
+### Link to source code
+
+In case of an output format that supports links (e.g. HTML), the embedded image generated by `pandoc-plot` can show a link to the source code which was used to generate the file. Therefore, other people can see what code was used to create your figures. 
+
+You can turn this off via the `source=true` key:
+
+__Markdown__:
+
+~~~markdown
+```{.mathplot source=true}
+...
+```
+~~~
+
+__LaTex__:
+
+```latex
+\begin{minted}[source=true]{mathplot}
+...
+\end{minted}
+```
+
+or via a [configuration file](#Configuration).
+
+### Preamble scripts
+
+If you find yourself always repeating some steps, inclusion of scripts is possible using the `preamble` parameter. For example, if you want all Matplotlib 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:
+
+```python
+import matplotlib.pyplot as plt
+plt.style.use('ggplot')
+```
+
+and include it in your document as follows:
+
+~~~markdown
+```{.matplotlib preamble=style.py}
+plt.figure()
+plt.plot([0,1,2,3,4], [1,2,3,4,5])
+plt.title('This is an example figure')
+```
+~~~
+
+Which is equivalent to writing the following markdown:
+
+~~~markdown
+```{.matplotlib}
+import matplotlib.pyplot as plt
+plt.style.use('ggplot')
+
+plt.figure()
+plt.plot([0,1,2,3,4], [1,2,3,4,5])
+plt.title('This is an example figure')
+```
+~~~
+
+The equivalent LaTeX usage is as follows:
+
+```latex
+\begin{minted}[include=style.py]{matplotlib}
+
+\end{minted}
+```
+
+This `preamble` 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.
+
+### No wasted work
+
+`pandoc-plot` minimizes work, only generating figures if it absolutely must, i.e. if the content has changed. 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.
+
+### Compatibility with pandoc-crossref
+
+[`pandoc-crossref`](https://github.com/lierdakil/pandoc-crossref) is a pandoc filter that makes it effortless to cross-reference objects in Markdown documents. 
+
+You can use `pandoc-crossref` in conjunction with `pandoc-plot` for the ultimate figure-making pipeline. You can combine both in a figure like so:
+
+~~~markdown
+```{#fig:myexample .plotly_python caption="This is a caption"}
+# Insert figure script here
+```
+
+As you can see in @fig:myexample, ...
+~~~
+
+If the above source is located in file `myfile.md`, you can render the figure and references by applying `pandoc-plot` **first**, and then `pandoc-crossref`. For example:
+
+```bash
+pandoc --filter pandoc-plot --filter pandoc-crossref -i myfile.md -o myfile.html
+```
+
+## Configuration
+
+To avoid repetition, `pandoc-plot` can be configured using simple YAML files. `pandoc-plot` will look for a `.pandoc-plot.yml` file in the current working directory. Here are **all** the possible parameters:
+
+```yaml
+# The following parameters affect all toolkits
+directory: plots/
+source: false
+dpi: 80
+format: PNG
+python_interpreter: python
+
+# The possible parameters for the Matplotlib toolkit
+matplotlib:
+  preamble: matplotlib.py
+  tight_bbox: false
+  transparent: false
+  executable: python
+
+# The possible parameters for the MATLAB toolkit
+matlabplot:
+  preamble: matlab.m
+  executable: matlab
+
+# The possible parameters for the Plotly/Python toolkit
+plotly_python:
+  preamble: plotly-python.py
+  executable: python
+
+# The possible parameters for the Mathematica toolkit
+mathplot:
+  preamble: mathematica.m
+  executable: math
+
+# The possible parameters for the GNU Octave toolkit
+octaveplot:
+  preamble: octave.m
+  executable: octave
+
+# The possible parameters for the ggplot2 toolkit
+ggplot2:
+  preamble: ggplot2.r
+  executable: Rscript
+```
+
+A file like the above sets the **default** values; you can still override them in documents directly.
+
+Using `pandoc-plot --write-example-config` will write the default configuration to a file which you can then customize.
+
+### Executables
+
+The `executable` parameter for all toolkits can be either the executable name (if it is present on the PATH), or the full path to the executable.
+
+Examples:
+
+```yaml
+matplotlib:
+  executable: python3
+```
+
+```yaml
+matlabplot:
+  executable: "C:\Program Files\Matlab\R2019b\bin\matlab.exe"
+```
+
+### Toolkit-specific options
+
+#### Matplotlib
+
+* `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.
+* `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`.
+
+## Installation
+
+### Binaries
+
+Windows binaries are available on [GitHub](https://github.com/LaurentRDC/pandoc-plot/releases). Place the executable in a location that is in your PATH to be able to call it.
+
+If you can show me how to generate binaries for other platform using e.g. Azure Pipelines, let me know!
+
+### Installers (Windows)
+
+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-plot/releases/latest).
+
+### From Hackage/Stackage
+
+*Coming soon*
+
+### From source
+
+Building from source can be done using [`stack`](https://docs.haskellstack.org/en/stable/README/) or [`cabal`](https://www.haskell.org/cabal/):
+
+```bash
+git clone https://github.com/LaurentRDC/pandoc-plot
+cd pandoc-plot
+stack install # Alternatively, `cabal install`
+```
+
+## Warning
+
+Do not run this filter on unknown documents. There is nothing in `pandoc-plot` that can stop a script from performing **evil actions**.
+ Setup.hs view
@@ -0,0 +1,7 @@+-- This script is used to build and install your package. Typically you don't
+-- need to change it. The Cabal documentation has more information about this
+-- file: <https://www.haskell.org/cabal/users-guide/installing-packages.html>.
+import qualified Distribution.Simple
+
+main :: IO ()
+main = Distribution.Simple.defaultMain
+ executable/ExampleConfig.hs view
@@ -0,0 +1,31 @@+{-# LANGUAGE TemplateHaskellQuotes #-}
+
+module ExampleConfig ( embedExampleConfig ) where
+
+
+import           Control.DeepSeq            (($!!))
+
+import           Data.String
+
+import           Language.Haskell.TH.Syntax
+
+import System.FilePath                      (FilePath)
+import System.IO
+
+docFile :: FilePath
+docFile = "example-config.yml"
+
+readDocFile :: IO String
+readDocFile = withFile docFile ReadMode $ \h -> do
+    hSetEncoding h utf8
+    cont <- hGetContents h
+    return $!! cont
+
+embedExampleConfig :: Q Exp
+embedExampleConfig = do
+    qAddDependentFile docFile
+    s <- runIO readDocFile
+    strToExp s
+    where
+        strToExp :: String -> Q Exp
+        strToExp s = return $ VarE 'fromString `AppE` LitE (StringL s)
+ executable/Main.hs view
@@ -0,0 +1,148 @@+{-# LANGUAGE ApplicativeDo   #-}
+{-# LANGUAGE LambdaCase      #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Main where
+
+import           Control.Applicative              ((<|>))
+import           Control.Monad                    (join)
+
+import           Data.Default.Class               (def)
+import           Data.List                        (intersperse)
+import           Data.Monoid                      ((<>))
+import qualified Data.Text                        as T
+import qualified Data.Text.IO                     as T
+
+import           Options.Applicative
+import qualified Options.Applicative.Help.Pretty  as P
+
+import           System.Directory                 (doesFileExist)
+import           System.IO.Temp                   (writeSystemTempFile)
+
+import           Text.Pandoc.Filter.Plot          (availableToolkits,
+                                                   plotTransform,
+                                                   unavailableToolkits)
+import           Text.Pandoc.Filter.Plot.Internal (Toolkit (..), cls, Configuration(..),
+                                                   supportedSaveFormats, 
+                                                   configuration)
+
+import           Text.Pandoc.JSON                 (toJSONFilter)
+
+import           Web.Browser                      (openBrowser)
+
+import qualified Data.Version                     as V
+import           Paths_pandoc_plot                (version)
+
+import           ManPage                          (embedManualHtml)
+import           ExampleConfig                    (embedExampleConfig)
+
+main :: IO ()
+main = join $ execParser opts
+    where
+        opts = info (run <**> helper)
+            (fullDesc
+            <> progDesc "This pandoc filter generates plots from code blocks using a multitude of possible renderers. This allows to keep documentation and figures in perfect synchronicity."
+            <> header "pandoc-plot - generate figures directly in documents using your plotting toolkit of choice."
+            <> footerDoc (Just footer')
+            )
+
+
+toJSONFilterWithConfig :: IO ()
+toJSONFilterWithConfig = do
+    c <- config
+    toJSONFilter (plotTransform c)
+
+
+config :: IO Configuration
+config = do 
+    configExists <- doesFileExist ".pandoc-plot.yml"
+    if configExists
+        then configuration ".pandoc-plot.yml" 
+        else return def
+
+
+data Flag = Version
+          | Manual
+          | Toolkits
+          | Config
+    deriving (Eq)
+
+
+run :: Parser (IO ())
+run = do
+    versionP <- flag Nothing (Just Version) (mconcat
+        [ long "version"
+        , short 'v'
+        , help "Show version number and exit."
+        ])
+
+    manualP  <- flag Nothing (Just Manual) (mconcat
+        [ long "manual"
+        , short 'm'
+        , help "Open the manual page in the default web browser and exit."
+        ])
+
+    toolkitsP <- flag Nothing (Just Toolkits) (mconcat
+        [ long "toolkits"
+        , short 't'
+        , help "Show information on toolkits and exit. Executables from the configuration \
+               \file will be used, if a '.pandoc-plot.yml' file is in the current directory."
+        ])
+    
+    configP <- flag Nothing (Just Config) (mconcat
+        [ long "write-example-config"
+        , help "Write an example configuration in '.pandoc-plot.yml', \
+               \which you can subsequently customize, and exit. If '.pandoc-plot.yml' \
+               \already exists, an error will be thrown. "])
+
+    input    <- optional $ strArgument (metavar "AST")
+    return $ go (versionP <|> manualP <|> toolkitsP <|> configP) input
+    where
+        go :: Maybe Flag -> Maybe String -> IO ()
+        go (Just Version)  _ = putStrLn (V.showVersion version)
+        go (Just Manual)   _ = writeSystemTempFile "pandoc-plot-manual.html" (T.unpack manualHtml)
+                                >>= \fp -> openBrowser ("file:///" <> fp)
+                                >> return ()
+        go (Just Toolkits) _ = do
+            c <- config
+            putStrLn "\nAVAILABLE TOOLKITS\n"
+            availableToolkits c >>= mapM_ toolkitInfo
+            putStrLn "\nUNAVAILABLE TOOLKITS\n"
+            unavailableToolkits c >>= mapM_ toolkitInfo
+        go (Just Config)   _ = T.writeFile ".example-pandoc-plot.yml" exampleConfig
+
+        go Nothing         _ = toJSONFilterWithConfig
+
+
+manualHtml :: T.Text
+manualHtml = T.pack $(embedManualHtml)
+
+
+exampleConfig :: T.Text
+exampleConfig = T.pack $(embedExampleConfig)
+
+
+toolkitInfo :: Toolkit -> IO ()
+toolkitInfo tk = do
+    putStrLn $ "Toolkit: " <> show tk
+    putStrLn $ "    Code block trigger: " <> (T.unpack . cls $ tk)
+    putStrLn $ "    Supported save formats: " <> (mconcat . intersperse ", " . fmap show $ supportedSaveFormats tk)
+    putStrLn mempty
+
+
+-- | Use Doc type directly because of newline formatting
+footer' :: P.Doc
+footer' = mconcat [
+        P.text "Example usage with pandoc:"
+    , P.line, P.line
+    , P.indent 4 $ P.string "> pandoc --filter pandoc-plot input.md --output output.html"
+    , P.line, P.line
+    , P.text "If you use pandoc-plot in combination with other filters, you probably want to run pandoc-plot first. Here is an example with pandoc-crossref:"
+    , P.line, P.line
+    , P.indent 4 $ P.string "> pandoc --filter pandoc-plot --filter pandoc-crossref -i input.md -o output.pdf"
+    , P.line, P.line
+    , P.text "More information can be found via the manual (pandoc-plot --manual) or the repository README, located at"
+    , P.line
+    , P.indent 4 $ P.text "https://github.com/LaurentRDC/pandoc-plot"
+    , P.line
+    ]
+ executable/ManPage.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE TemplateHaskellQuotes #-}
+{-|
+This module was inspired by pandoc-crossref
+|-}
+
+module ManPage ( embedManualHtml ) where
+
+import           Control.DeepSeq            (($!!))
+
+import           Data.String 
+import qualified Data.Text                  as T
+
+import           Language.Haskell.TH.Syntax
+
+import qualified Text.Pandoc                as P
+import           Text.Pandoc.Highlighting   (pygments)
+
+import System.FilePath                      (FilePath)
+import System.IO
+
+docFile :: FilePath
+docFile = "README.md"
+
+readDocFile :: IO String
+readDocFile = withFile docFile ReadMode $ \h -> do
+    hSetEncoding h utf8
+    cont <- hGetContents h
+    return $!! cont
+
+readerOpts :: P.ReaderOptions
+readerOpts = P.def { P.readerExtensions = P.githubMarkdownExtensions
+                   , P.readerStandalone = True
+                   }
+
+embedManual :: (P.Pandoc -> P.PandocPure T.Text) -> Q Exp
+embedManual fmt = do
+    qAddDependentFile docFile
+    d <- runIO readDocFile
+    let pd  = either (error . show) id $ P.runPure $ P.readMarkdown readerOpts (T.pack d)
+        txt = either (error . show) id $ P.runPure $ fmt pd
+    strToExp $ T.unpack txt
+    where
+        strToExp :: String -> Q Exp
+        strToExp s = return $ VarE 'fromString `AppE` LitE (StringL s)
+
+embedManualHtml :: Q Exp
+embedManualHtml = do
+    embedManual $ P.writeHtml5String P.def { P.writerHighlightStyle = Just pygments }
+ pandoc-plot.cabal view
@@ -0,0 +1,108 @@+name:           pandoc-plot
+version:        0.1.0.0
+cabal-version:  >= 1.12
+synopsis:       A Pandoc filter to include figures generated from code blocks using your plotting toolkit of choice.
+description:    A Pandoc filter to include figures generated from code blocks. Keep the document and code in the same location. Output is captured and included as a figure.
+category:       Documentation
+homepage:       https://github.com/LaurentRDC/pandoc-plot#readme
+bug-reports:    https://github.com/LaurentRDC/pandoc-plot/issues
+author:         Laurent P. René de Cotret
+maintainer:     Laurent P. René de Cotret
+license:        GPL-2
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    CHANGELOG.md
+    LICENSE
+    README.md
+    stack.yaml
+
+source-repository head
+    type: git
+    location: https://github.com/LaurentRDC/pandoc-plot
+
+library
+    exposed-modules:
+        Text.Pandoc.Filter.Plot
+        Text.Pandoc.Filter.Plot.Internal
+    other-modules:
+        Paths_pandoc_plot
+        Text.Pandoc.Filter.Plot.Configuration
+        Text.Pandoc.Filter.Plot.Types
+        Text.Pandoc.Filter.Plot.Parse
+        Text.Pandoc.Filter.Plot.Scripting
+        Text.Pandoc.Filter.Plot.Renderers
+        Text.Pandoc.Filter.Plot.Renderers.Prelude
+        Text.Pandoc.Filter.Plot.Renderers.Matplotlib
+        Text.Pandoc.Filter.Plot.Renderers.Plotly
+        Text.Pandoc.Filter.Plot.Renderers.Matlab
+        Text.Pandoc.Filter.Plot.Renderers.Mathematica
+        Text.Pandoc.Filter.Plot.Renderers.Octave
+        Text.Pandoc.Filter.Plot.Renderers.GGPlot2
+    hs-source-dirs:
+        src
+    ghc-options: -Wall -Wcompat
+    build-depends:
+          base               >= 4.11  && <5
+        , containers
+        , directory
+        , data-default-class >= 0.1.2 && < 0.2
+        , filepath           >= 1.4   && < 2
+        , hashable           >= 1     && < 2
+        , pandoc             >= 2.8   && < 3
+        , pandoc-types       >= 1.20  && < 2
+        , shakespeare        >= 2.0   && < 3
+        , temporary
+        , text               >= 1     && < 2
+        , typed-process      >= 0.2.1 && < 1
+        , yaml               >= 0.8   && < 1
+        , mtl                >= 2.2   && < 2.3
+    default-language: Haskell2010
+
+executable pandoc-plot
+    main-is: Main.hs
+    other-modules:
+        ManPage
+        ExampleConfig
+        Paths_pandoc_plot
+    hs-source-dirs:
+        executable
+    ghc-options: -Wall -Wcompat -rtsopts -threaded -with-rtsopts=-N
+    build-depends:
+          base                  >=4.11 && <5
+        , directory
+        , data-default-class    >= 0.1.2
+        , deepseq
+        , filepath
+        , open-browser          >= 0.2.1.0
+        , optparse-applicative  >= 0.14 && < 1
+        , pandoc
+        , pandoc-plot
+        , pandoc-types          >1.12 && <2
+        , template-haskell      > 2.7 && < 3
+        , temporary
+        , text
+    default-language: Haskell2010
+
+test-suite tests
+    type: exitcode-stdio-1.0
+    hs-source-dirs:  tests
+    main-is:         Main.hs
+    other-modules:
+        Common
+    build-depends:   base                 >= 4.11 && < 5
+                   , directory
+                   , data-default-class   >= 0.1.2
+                   , filepath
+                   , hspec
+                   , hspec-expectations
+                   , pandoc-types         >= 1.20 && <= 2
+                   , pandoc-plot
+                   , tasty
+                   , tasty-hunit
+                   , tasty-hspec
+                   , temporary
+                   , text
+                   , mtl                >= 2.2   && < 2.3
+    default-language: Haskell2010
+  
+ src/Text/Pandoc/Filter/Plot.hs view
@@ -0,0 +1,108 @@+{-# LANGUAGE MultiWayIf        #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+{-|
+Module      : $header$
+Description : Pandoc filter to create figures from code blocks using your plotting toolkit of choice
+Copyright   : (c) Laurent P René de Cotret, 2020
+License     : GNU GPL, version 2 or above
+Maintainer  : laurent.decotret@outlook.com
+Stability   : unstable
+Portability : portable
+
+This module defines a Pandoc filter @makePlot@ and related functions
+that can be used to walk over a Pandoc document and generate figures from
+code blocks using a multitude of plotting toolkits.
+
+The syntax for code blocks is simple, Code blocks with the appropriate class
+attribute will trigger the filter. For example:
+
+*   @.matplotlib@ for matplotlib-based Python plots;
+*   @.plotly_python@ for Plotly-based Python plots;
+*   @.matlabplot@ for MATLAB plots;
+*   @.mathplot@ for Mathematica plots;
+*   @.octaveplot@ for GNU Octave plots;
+*   @.ggplot2@ for ggplot2-based R plots;
+
+The code block will be reworked into a script and the output figure will be captured. Optionally, the source code
+ used to generate the figure will be linked in the caption.
+
+Here are the possible attributes what pandoc-plot understands for ALL toolkits:
+
+    * @directory=...@ : Directory where to save the figure.
+    * @source=true|false@ : Whether or not to link the source code of this figure in the caption. Ideal for web pages, for example. Default is false.
+    * @format=...@: Format of the generated figure. This can be an extension or an acronym, e.g. @format=png@.
+    * @caption="..."@: Specify a plot caption (or alternate text). Captions support Markdown formatting and LaTeX math (@$...$@).
+    * @dpi=...@: Specify a value for figure resolution, or dots-per-inch. Certain toolkits ignore this.
+    * @preamble=...@: Path to a file to include before the code block. Ideal to avoid repetition over many figures.
+
+Default values for the above attributes are stored in the @Configuration@ datatype. These can be specified in a YAML file.
+-}
+module Text.Pandoc.Filter.Plot (
+    -- * Operating on single Pandoc blocks
+      makePlot
+    -- * Operating on whole Pandoc documents
+    , plotTransform
+    -- * Runtime configuration
+    , configuration
+    , Configuration(..)
+    , SaveFormat(..)
+    , Script
+    -- * For testing purposes ONLY
+    , make
+    , availableToolkits
+    , unavailableToolkits
+    ) where
+
+import           Control.Monad.Reader             (runReaderT)
+
+import           Text.Pandoc.Definition
+import           Text.Pandoc.Walk                 (walkM)
+
+import           Text.Pandoc.Filter.Plot.Internal
+
+
+-- | Highest-level function that can be walked over a Pandoc tree.
+-- All code blocks that have the @.plot@ / @.plotly@ class will be considered
+-- figures.
+makePlot :: Configuration -> Block -> IO Block
+makePlot conf block = 
+    maybe (return block) (\tk -> make tk conf block) (plotToolkit block)
+
+
+-- | Walk over an entire Pandoc document, changing appropriate code blocks
+-- into figures. Default configuration is used.
+plotTransform :: Configuration -> Pandoc -> IO Pandoc
+plotTransform = walkM . makePlot
+
+
+-- | Force to use a particular toolkit to render appropriate code blocks.
+make :: Toolkit -> Configuration -> Block -> IO Block
+make tk conf block = do
+    let runEnv = PlotEnv tk conf
+    runReaderT (makePlot' block >>= either (fail . show) return) runEnv
+    where
+        makePlot' blk = do
+            parsed <- parseFigureSpec blk
+            maybe
+                (return $ Right blk)
+                (\s -> handleResult s <$> runScriptIfNecessary s)
+                parsed
+            where
+                handleResult _ (ScriptChecksFailed msg) = Left  $ ScriptChecksFailedError msg
+                handleResult _ (ScriptFailure cmd code) = Left  $ ScriptError cmd code
+                handleResult spec ScriptSuccess         = Right $ toImage spec
+
+
+-- | Possible errors returned by the filter
+data PandocPlotError
+    = ScriptError String Int          -- ^ Running script has yielded an error
+    | ScriptChecksFailedError String  -- ^ Script did not pass all checks
+    deriving (Eq)
+
+instance Show PandocPlotError where
+    show (ScriptError cmd exitcode)   = mconcat [ "Script error: plot could not be generated.\n"
+                                                , "    Command: ", cmd, "\n"
+                                                , "    Exit code " <> (show exitcode)
+                                                ]
+    show (ScriptChecksFailedError msg) = "Script did not pass all checks: " <> msg
+ src/Text/Pandoc/Filter/Plot/Configuration.hs view
@@ -0,0 +1,174 @@+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards   #-}
+
+
+{-|
+Module      : $header$
+Copyright   : (c) Laurent P René de Cotret, 2020
+License     : GNU GPL, version 2 or above
+Maintainer  : laurent.decotret@outlook.com
+Stability   : internal
+Portability : portable
+
+Reading configuration from file
+-}
+
+module Text.Pandoc.Filter.Plot.Configuration (
+      configuration
+) where
+
+import           Data.Default.Class     (Default, def)
+import           Data.Maybe             (fromMaybe)
+import           Data.Text              (Text, pack)
+import qualified Data.Text.IO           as TIO
+import           Data.Yaml
+import           Data.Yaml.Config       (ignoreEnv, loadYamlSettings)
+
+import Text.Pandoc.Filter.Plot.Types
+
+-- | Read configuration from a YAML file. The
+-- keys are exactly the same as for code blocks.
+--
+-- If a key is either not present, its value will be set
+-- to the default value. Parsing errors result in thrown exceptions.
+configuration :: FilePath -> IO Configuration
+configuration fp = (loadYamlSettings [fp] [] ignoreEnv) >>= renderConfig
+
+
+-- We define a precursor type because preambles are best specified as file paths,
+-- but we want to read those files before building a full
+-- @Configuration@ value.
+data ConfigPrecursor = ConfigPrecursor
+    { _defaultDirectory  :: !FilePath   -- ^ The default directory where figures will be saved.
+    , _defaultWithSource :: !Bool       -- ^ The default behavior of whether or not to include links to source code and high-res
+    , _defaultDPI        :: !Int        -- ^ The default dots-per-inch value for generated figures. Renderers might ignore this.
+    , _defaultSaveFormat :: !SaveFormat -- ^ The default save format of generated figures.
+    
+    , _matplotlibPrec    :: !MatplotlibPrecursor
+    , _matlabPrec        :: !MatlabPrecursor
+    , _plotlyPythonPrec  :: !PlotlyPythonPrecursor
+    , _mathematicaPrec   :: !MathematicaPrecursor
+    , _octavePrec        :: !OctavePrecursor
+    , _ggplot2Prec       :: !GGPlot2Precursor
+    }
+
+instance Default ConfigPrecursor where
+    def = ConfigPrecursor
+        { _defaultDirectory  = defaultDirectory def
+        , _defaultWithSource = defaultWithSource def
+        , _defaultDPI        = defaultDPI def
+        , _defaultSaveFormat = defaultSaveFormat def
+        
+        , _matplotlibPrec    = def
+        , _matlabPrec        = def
+        , _plotlyPythonPrec  = def
+        , _mathematicaPrec   = def
+        , _octavePrec        = def
+        , _ggplot2Prec       = def
+        }
+
+
+-- Separate YAML clauses have their own types.
+data MatplotlibPrecursor = MatplotlibPrecursor
+        { _matplotlibPreamble    :: !(Maybe FilePath)
+        , _matplotlibTightBBox   :: !Bool
+        , _matplotlibTransparent :: !Bool
+        , _matplotlibExe         :: !FilePath
+        }
+data MatlabPrecursor        = MatlabPrecursor       {_matlabPreamble       :: !(Maybe FilePath), _matlabExe       :: !FilePath}
+data PlotlyPythonPrecursor  = PlotlyPythonPrecursor {_plotlyPythonPreamble :: !(Maybe FilePath), _plotlyPythonExe :: !FilePath}
+data MathematicaPrecursor   = MathematicaPrecursor  {_mathematicaPreamble  :: !(Maybe FilePath), _mathematicaExe  :: !FilePath}
+data OctavePrecursor        = OctavePrecursor       {_octavePreamble       :: !(Maybe FilePath), _octaveExe       :: !FilePath}
+data GGPlot2Precursor       = GGPlot2Precursor      {_ggplot2Preamble      :: !(Maybe FilePath), _ggplot2Exe      :: !FilePath}
+
+
+instance Default MatplotlibPrecursor where
+    def = MatplotlibPrecursor Nothing (matplotlibTightBBox def) (matplotlibTransparent def) (matplotlibExe def)
+
+instance Default MatlabPrecursor        where def = MatlabPrecursor       Nothing (matlabExe def)
+instance Default PlotlyPythonPrecursor  where def = PlotlyPythonPrecursor Nothing (plotlyPythonExe def)
+instance Default MathematicaPrecursor   where def = MathematicaPrecursor  Nothing (mathematicaExe def)
+instance Default OctavePrecursor        where def = OctavePrecursor       Nothing (octaveExe def)
+instance Default GGPlot2Precursor       where def = GGPlot2Precursor      Nothing (ggplot2Exe def)
+
+instance FromJSON MatplotlibPrecursor where
+    parseJSON (Object v) = 
+        MatplotlibPrecursor
+            <$> v .:? (tshow MatplotlibPreambleK)
+            <*> v .:? (tshow MatplotlibTightBBoxK)   .!= (matplotlibTightBBox def) 
+            <*> v .:? (tshow MatplotlibTransparentK) .!= (matplotlibTransparent def)
+            <*> v .:? (tshow MatplotlibExecutableK)  .!= (matplotlibExe def)
+    parseJSON _ = fail $ mconcat ["Could not parse ", show Matplotlib, " configuration."]
+
+instance FromJSON MatlabPrecursor where
+    parseJSON (Object v) = MatlabPrecursor <$> v .:? (tshow MatlabPreambleK) <*> v .:? (tshow MatlabExecutableK) .!= (matlabExe def)
+    parseJSON _ = fail $ mconcat ["Could not parse ", show Matlab, " configuration."]
+
+instance FromJSON PlotlyPythonPrecursor where
+    parseJSON (Object v) = PlotlyPythonPrecursor <$> v .:? (tshow PlotlyPythonPreambleK) <*> v .:? (tshow PlotlyPythonExecutableK) .!= (plotlyPythonExe def)
+    parseJSON _ = fail $ mconcat ["Could not parse ", show PlotlyPython, " configuration."]
+
+instance FromJSON MathematicaPrecursor where
+    parseJSON (Object v) = MathematicaPrecursor <$> v .:? (tshow MathematicaPreambleK) <*> v .:? (tshow MathematicaExecutableK) .!= (mathematicaExe def)
+    parseJSON _ = fail $ mconcat ["Could not parse ", show Mathematica, " configuration."]
+
+instance FromJSON OctavePrecursor where
+    parseJSON (Object v) = OctavePrecursor <$> v .:? (tshow OctavePreambleK) <*> v .:? (tshow OctaveExecutableK) .!= (octaveExe def)
+    parseJSON _ = fail $ mconcat ["Could not parse ", show Octave, " configuration."]
+
+instance FromJSON GGPlot2Precursor where
+    parseJSON (Object v) = GGPlot2Precursor <$> v .:? (tshow GGPlot2PreambleK) <*> v .:? (tshow GGPlot2ExecutableK) .!= (ggplot2Exe def)
+    parseJSON _ = fail $ mconcat ["Could not parse ", show GGPlot2, " configuration."]
+
+
+instance FromJSON ConfigPrecursor where
+    parseJSON (Null) = return def -- In case of empty file
+    parseJSON (Object v) = do
+        
+        _defaultDirectory  <- v .:? (tshow DirectoryK)     .!= (defaultDirectory def)
+        _defaultWithSource <- v .:? (tshow WithSourceK)    .!= (defaultWithSource def)
+        _defaultDPI        <- v .:? (tshow DpiK)           .!= (defaultDPI def)
+        _defaultSaveFormat <- v .:? (tshow SaveFormatK)    .!= (_defaultSaveFormat def)
+
+        _matplotlibPrec    <- v .:? (cls Matplotlib)       .!= def
+        _matlabPrec        <- v .:? (cls Matlab)           .!= def
+        _plotlyPythonPrec  <- v .:? (cls PlotlyPython)     .!= def
+        _mathematicaPrec   <- v .:? (cls Mathematica)      .!= def
+        _octavePrec        <- v .:? (cls Octave)           .!= def
+        _ggplot2Prec       <- v .:? (cls GGPlot2)          .!= def
+
+        return $ ConfigPrecursor{..}
+    parseJSON _          = fail "Could not parse configuration."
+
+
+renderConfig :: ConfigPrecursor -> IO Configuration
+renderConfig ConfigPrecursor{..} = do
+    let defaultDirectory  = _defaultDirectory
+        defaultWithSource = _defaultWithSource
+        defaultDPI        = _defaultDPI
+        defaultSaveFormat = _defaultSaveFormat
+
+        matplotlibTightBBox   = _matplotlibTightBBox _matplotlibPrec
+        matplotlibTransparent = _matplotlibTransparent _matplotlibPrec
+
+        matplotlibExe   = _matplotlibExe _matplotlibPrec
+        matlabExe       = _matlabExe _matlabPrec
+        plotlyPythonExe = _plotlyPythonExe _plotlyPythonPrec
+        mathematicaExe  = _mathematicaExe _mathematicaPrec
+        octaveExe       = _octaveExe _octavePrec
+        ggplot2Exe      = _ggplot2Exe _ggplot2Prec
+    
+    matplotlibPreamble   <- readPreamble (_matplotlibPreamble _matplotlibPrec)
+    matlabPreamble       <- readPreamble (_matlabPreamble _matlabPrec)
+    plotlyPythonPreamble <- readPreamble (_plotlyPythonPreamble _plotlyPythonPrec)
+    mathematicaPreamble  <- readPreamble (_mathematicaPreamble _mathematicaPrec)
+    octavePreamble       <- readPreamble (_octavePreamble _octavePrec)
+    ggplot2Preamble      <- readPreamble (_ggplot2Preamble _ggplot2Prec)
+
+    return Configuration{..}
+    where
+        readPreamble fp = fromMaybe mempty $ TIO.readFile <$> fp
+
+
+tshow :: Show a => a -> Text
+tshow = pack . show
+ src/Text/Pandoc/Filter/Plot/Internal.hs view
@@ -0,0 +1,26 @@+
+{-|
+Module      : $header$
+Copyright   : (c) Laurent P René de Cotret, 2020
+License     : GNU GPL, version 2 or above
+Maintainer  : laurent.decotret@outlook.com
+Stability   : internal
+Portability : portable
+
+This module re-exports internal pandoc-plot functionality.
+The external use of content from this module is discouraged.
+-}
+
+module Text.Pandoc.Filter.Plot.Internal (
+      module Text.Pandoc.Filter.Plot.Types
+    , module Text.Pandoc.Filter.Plot.Renderers
+    , module Text.Pandoc.Filter.Plot.Scripting
+    , module Text.Pandoc.Filter.Plot.Parse
+    , module Text.Pandoc.Filter.Plot.Configuration
+ ) where
+
+import           Text.Pandoc.Filter.Plot.Parse
+import           Text.Pandoc.Filter.Plot.Renderers
+import           Text.Pandoc.Filter.Plot.Scripting
+import           Text.Pandoc.Filter.Plot.Types
+import           Text.Pandoc.Filter.Plot.Configuration
+ src/Text/Pandoc/Filter/Plot/Parse.hs view
@@ -0,0 +1,147 @@+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE RecordWildCards       #-}
+{-# LANGUAGE TemplateHaskell       #-}
+
+{-|
+Module      : $header$
+Copyright   : (c) Laurent P René de Cotret, 2020
+License     : GNU GPL, version 2 or above
+Maintainer  : laurent.decotret@outlook.com
+Stability   : internal
+Portability : portable
+
+This module defines types and functions that help
+with keeping track of figure specifications
+-}
+module Text.Pandoc.Filter.Plot.Parse (
+      plotToolkit
+    , parseFigureSpec
+    , captionReader
+) where
+
+import           Control.Monad                     (join, when)
+import           Control.Monad.Reader              (asks, liftIO)
+
+import           Data.Default.Class                (def)
+import           Data.List                         (intersperse)
+import qualified Data.Map.Strict                   as Map
+import           Data.Maybe                        (fromMaybe, listToMaybe)
+import           Data.Monoid                       ((<>))
+import           Data.String                       (fromString)
+import           Data.Text                         (Text, pack, unpack)
+import qualified Data.Text.IO                      as TIO
+import           Data.Version                      (showVersion)
+
+import           Paths_pandoc_plot                 (version)
+
+import           System.FilePath                   (makeValid)
+
+import           Text.Pandoc.Definition            (Block (..), Inline,
+                                                    Pandoc (..))
+
+import           Text.Pandoc.Class                 (runPure)
+import           Text.Pandoc.Extensions            (Extension (..),
+                                                    extensionsFromList)
+import           Text.Pandoc.Options               (ReaderOptions (..))
+import           Text.Pandoc.Readers               (readMarkdown)
+
+import           Text.Pandoc.Filter.Plot.Renderers
+import           Text.Pandoc.Filter.Plot.Types
+
+tshow :: Show a => a -> Text
+tshow = pack . show
+
+-- | Determine inclusion specifications from @Block@ attributes.
+-- If an environment is detected, but the save format is incompatible,
+-- an error will be thrown.
+parseFigureSpec :: Block -> PlotM (Maybe FigureSpec)
+parseFigureSpec (CodeBlock (id', classes, attrs) content) = do
+    toolkit <- asks toolkit
+    if not (cls toolkit `elem` classes)
+        then return Nothing
+        else Just <$> figureSpec
+
+    where
+        attrs'        = Map.fromList attrs
+        preamblePath  = unpack <$> Map.lookup (tshow PreambleK) attrs'
+
+        figureSpec :: PlotM FigureSpec
+        figureSpec = do
+            conf <- asks config
+            toolkit <- asks toolkit
+            let extraAttrs' = parseExtraAttrs toolkit attrs'
+                header = comment toolkit $ "Generated by pandoc-plot " <> ((pack . showVersion) version)
+                defaultPreamble = preambleSelector toolkit conf
+            -- Note that the default preamble changes based on the RendererM
+            -- which is why we use @preambleSelector@ as the default value
+            includeScript <- fromMaybe
+                                (return defaultPreamble)
+                                ((liftIO . TIO.readFile) <$> preamblePath)
+            let -- Filtered attributes that are not relevant to pandoc-plot
+                -- This presumes that inclusionKeys includes ALL possible keys, for all renderers
+                filteredAttrs = filter (\(k, _) -> k `notElem` (tshow <$> inclusionKeys)) attrs
+                defWithSource = defaultWithSource conf
+                defSaveFmt = defaultSaveFormat conf
+                defDPI = defaultDPI conf
+
+            let caption        = Map.findWithDefault mempty (tshow CaptionK) attrs'
+                withSource     = fromMaybe defWithSource $ readBool <$> Map.lookup (tshow WithSourceK) attrs'
+                script         = mconcat $ intersperse "\n" [header, includeScript, content]
+                saveFormat     = fromMaybe defSaveFmt $ (fromString . unpack) <$> Map.lookup (tshow SaveFormatK) attrs'
+                directory      = makeValid $ unpack $ Map.findWithDefault (pack $ defaultDirectory conf) (tshow DirectoryK) attrs'
+                dpi            = fromMaybe defDPI $ (read . unpack) <$> Map.lookup (tshow DpiK) attrs'
+                extraAttrs     = Map.toList extraAttrs'
+                blockAttrs     = (id', classes, filteredAttrs)
+
+            -- This is the first opportunity to check save format compatibility
+            let saveFormatSupported = saveFormat `elem` (supportedSaveFormats toolkit)
+            when (not saveFormatSupported) $ do
+                (error $ mconcat ["Save format ", show saveFormat, " not supported by ", show toolkit ])
+            return FigureSpec{..}
+
+parseFigureSpec _ = return Nothing
+
+
+-- | Determine which toolkit should be used to render the plot
+-- from a code block, if any.
+plotToolkit :: Block -> Maybe Toolkit
+plotToolkit (CodeBlock (_, classes, _) _) =
+    listToMaybe $ filter (\tk->cls tk `elem` classes) toolkits
+plotToolkit _ = Nothing
+
+
+-- | Reader options for captions.
+readerOptions :: ReaderOptions
+readerOptions = def
+    {readerExtensions =
+        extensionsFromList
+            [ Ext_tex_math_dollars
+            , Ext_superscript
+            , Ext_subscript
+            , Ext_raw_tex
+            ]
+    }
+
+
+-- | Read a figure caption in Markdown format. LaTeX math @$...$@ is supported,
+-- as are Markdown subscripts and superscripts.
+captionReader :: Text -> Maybe [Inline]
+captionReader t = either (const Nothing) (Just . extractFromBlocks) $ runPure $ readMarkdown' t
+    where
+        readMarkdown' = readMarkdown readerOptions
+
+        extractFromBlocks (Pandoc _ blocks) = mconcat $ extractInlines <$> blocks
+
+        extractInlines (Plain inlines)          = inlines
+        extractInlines (Para inlines)           = inlines
+        extractInlines (LineBlock multiinlines) = join multiinlines
+        extractInlines _                        = []
+
+
+-- | Flexible boolean parsing
+readBool :: Text -> Bool
+readBool s | s `elem` ["True",  "true",  "'True'",  "'true'",  "1"] = True
+           | s `elem` ["False", "false", "'False'", "'false'", "0"] = False
+           | otherwise = error $ unpack $ mconcat ["Could not parse '", s, "' into a boolean. Please use 'True' or 'False'"]
+ src/Text/Pandoc/Filter/Plot/Renderers.hs view
@@ -0,0 +1,137 @@+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards   #-}
+
+{-|
+Module      : $header$
+Copyright   : (c) Laurent P René de Cotret, 2020
+License     : GNU GPL, version 2 or above
+Maintainer  : laurent.decotret@outlook.com
+Stability   : internal
+Portability : portable
+
+Specification of renderers.
+-}
+
+module Text.Pandoc.Filter.Plot.Renderers (
+      scriptExtension
+    , comment
+    , preambleSelector
+    , supportedSaveFormats
+    , scriptChecks
+    , parseExtraAttrs
+    , command
+    , capture
+    , availableToolkits
+    , unavailableToolkits
+) where
+
+import           Control.Monad                                 (filterM)
+
+import           Data.List                                     ((\\))
+import           Data.Map.Strict                               (Map)
+import           Data.Text                                     (Text)
+
+import           Text.Pandoc.Filter.Plot.Renderers.Mathematica
+import           Text.Pandoc.Filter.Plot.Renderers.Matlab
+import           Text.Pandoc.Filter.Plot.Renderers.Matplotlib
+import           Text.Pandoc.Filter.Plot.Renderers.Octave
+import           Text.Pandoc.Filter.Plot.Renderers.Plotly
+import           Text.Pandoc.Filter.Plot.Renderers.GGPlot2
+
+import           Text.Pandoc.Filter.Plot.Types
+
+
+-- Extension for script files, e.g. ".py", or ".m".
+scriptExtension :: Toolkit -> String
+scriptExtension Matplotlib   = ".py"
+scriptExtension PlotlyPython = ".py"
+scriptExtension Matlab       = ".m"
+scriptExtension Mathematica  = ".m"
+scriptExtension Octave       = ".m"
+scriptExtension GGPlot2      = ".r"
+
+
+-- Make a string into a comment
+comment :: Toolkit -> (Text -> Text)
+comment Matplotlib   = mappend "# "
+comment PlotlyPython = mappend "# "
+comment Matlab       = mappend "% "
+comment Mathematica  = \t -> mconcat ["(*", t, "*)"]
+comment Octave       = mappend "% "
+comment GGPlot2      = mappend "# "
+
+
+-- | The function that maps from configuration to the preamble.
+preambleSelector :: Toolkit -> (Configuration -> Script)
+preambleSelector Matplotlib   = matplotlibPreamble
+preambleSelector PlotlyPython = plotlyPythonPreamble
+preambleSelector Matlab       = matlabPreamble
+preambleSelector Mathematica  = mathematicaPreamble
+preambleSelector Octave       = octavePreamble
+preambleSelector GGPlot2      = ggplot2Preamble
+
+
+-- | Save formats supported by this renderer.
+supportedSaveFormats :: Toolkit -> [SaveFormat]
+supportedSaveFormats Matplotlib   = matplotlibSupportedSaveFormats
+supportedSaveFormats PlotlyPython = plotlyPythonSupportedSaveFormats
+supportedSaveFormats Matlab       = matlabSupportedSaveFormats
+supportedSaveFormats Mathematica  = mathematicaSupportedSaveFormats
+supportedSaveFormats Octave       = octaveSupportedSaveFormats
+supportedSaveFormats GGPlot2      = ggplot2SupportedSaveFormats
+
+
+-- Checks to perform before running a script. If ANY check fails,
+-- the figure is not rendered. This is to prevent, for example,
+-- blocking operations to occur.
+scriptChecks :: Toolkit -> [Script -> CheckResult]
+scriptChecks = const mempty
+
+
+-- | Parse code block headers for extra attributes that are specific
+-- to this renderer. By default, no extra attributes are parsed.
+parseExtraAttrs :: Toolkit -> Map Text Text -> Map Text Text
+parseExtraAttrs Matplotlib = matplotlibExtraAttrs
+parseExtraAttrs _          = return mempty
+
+
+-- | Generate the appropriate command-line command to generate a figure.
+command :: Toolkit -> (Configuration -> FigureSpec -> FilePath -> Text)
+command Matplotlib   = matplotlibCommand
+command PlotlyPython = plotlyPythonCommand
+command Matlab       = matlabCommand
+command Mathematica  = mathematicaCommand
+command Octave       = octaveCommand
+command GGPlot2      = ggplot2Command
+
+
+-- | Script fragment required to capture a figure.
+capture :: Toolkit -> (FigureSpec -> FilePath -> Script)
+capture Matplotlib   = matplotlibCapture
+capture PlotlyPython = plotlyPythonCapture
+capture Matlab       = matlabCapture
+capture Mathematica  = mathematicaCapture
+capture Octave       = octaveCapture
+capture GGPlot2      = ggplot2Capture
+
+
+-- | Check if a toolkit is available, based on the current configuration
+toolkitAvailable :: Toolkit -> Configuration -> IO Bool
+toolkitAvailable Matplotlib   = matplotlibAvailable
+toolkitAvailable PlotlyPython = plotlyPythonAvailable
+toolkitAvailable Matlab       = matlabAvailable
+toolkitAvailable Mathematica  = mathematicaAvailable
+toolkitAvailable Octave       = octaveAvailable
+toolkitAvailable GGPlot2      = ggplot2Available
+
+
+-- | List of toolkits available on this machine.
+-- The executables to look for are taken from the configuration.
+availableToolkits :: Configuration -> IO [Toolkit]
+availableToolkits conf = filterM (\tk -> toolkitAvailable tk conf) toolkits
+    
+
+-- | List of toolkits not available on this machine.
+-- The executables to look for are taken from the configuration.
+unavailableToolkits :: Configuration -> IO [Toolkit]
+unavailableToolkits conf = ((\\) toolkits) <$> availableToolkits conf
+ src/Text/Pandoc/Filter/Plot/Renderers/GGPlot2.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-|
+Module      : $header$
+Copyright   : (c) Laurent P René de Cotret, 2020
+License     : GNU GPL, version 2 or above
+Maintainer  : laurent.decotret@outlook.com
+Stability   : internal
+Portability : portable
+
+Rendering Mathematica plots code blocks
+-}
+
+module Text.Pandoc.Filter.Plot.Renderers.GGPlot2 (
+      ggplot2SupportedSaveFormats
+    , ggplot2Command
+    , ggplot2Capture
+    , ggplot2Available
+) where
+
+import           Text.Pandoc.Filter.Plot.Renderers.Prelude
+
+ggplot2SupportedSaveFormats :: [SaveFormat]
+ggplot2SupportedSaveFormats = [PNG, PDF, SVG, JPG, EPS, TIF]
+
+
+ggplot2Command :: Configuration -> FigureSpec -> FilePath -> Text
+ggplot2Command Configuration{..} _ fp = [st|#{ggplot2Exe} #{fp}|]
+
+
+ggplot2Available :: Configuration -> IO Bool
+ggplot2Available Configuration{..} = commandSuccess [st|#{ggplot2Exe} -e 'library("ggplot2")'|]
+
+
+ggplot2Capture :: FigureSpec -> FilePath -> Script
+ggplot2Capture FigureSpec{..} fname = [st|
+library(ggplot2) # just in case
+ggsave("#{fname}", plot = last_plot(), dpi = #{dpi})
+|]
+ src/Text/Pandoc/Filter/Plot/Renderers/Mathematica.hs view
@@ -0,0 +1,40 @@+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-|
+Module      : $header$
+Copyright   : (c) Laurent P René de Cotret, 2020
+License     : GNU GPL, version 2 or above
+Maintainer  : laurent.decotret@outlook.com
+Stability   : internal
+Portability : portable
+
+Rendering Mathematica plots code blocks
+-}
+
+module Text.Pandoc.Filter.Plot.Renderers.Mathematica (
+      mathematicaSupportedSaveFormats
+    , mathematicaCommand
+    , mathematicaCapture
+    , mathematicaAvailable
+) where
+
+import           Text.Pandoc.Filter.Plot.Renderers.Prelude
+
+mathematicaSupportedSaveFormats :: [SaveFormat]
+mathematicaSupportedSaveFormats = [PNG, PDF, SVG, JPG, EPS, GIF, TIF]
+
+
+mathematicaCommand :: Configuration -> FigureSpec -> FilePath -> Text
+mathematicaCommand Configuration{..} _ fp = [st|#{mathematicaExe} -script #{fp}|]
+
+
+mathematicaAvailable :: Configuration -> IO Bool
+mathematicaAvailable Configuration{..} = commandSuccess [st|#{mathematicaExe} -h|] -- TODO: test this
+
+
+mathematicaCapture :: FigureSpec -> FilePath -> Script
+mathematicaCapture FigureSpec{..} fname = [st|
+Export["#{fname}", %, show saveFormat]
+|]
+ src/Text/Pandoc/Filter/Plot/Renderers/Matlab.hs view
@@ -0,0 +1,40 @@+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-|
+Module      : $header$
+Copyright   : (c) Laurent P René de Cotret, 2020
+License     : GNU GPL, version 2 or above
+Maintainer  : laurent.decotret@outlook.com
+Stability   : internal
+Portability : portable
+
+Rendering Matlab code blocks
+-}
+
+module Text.Pandoc.Filter.Plot.Renderers.Matlab (
+      matlabSupportedSaveFormats
+    , matlabCommand
+    , matlabCapture
+    , matlabAvailable
+) where
+
+import           Text.Pandoc.Filter.Plot.Renderers.Prelude
+
+
+matlabSupportedSaveFormats :: [SaveFormat]
+matlabSupportedSaveFormats = [PNG, PDF, SVG, JPG, EPS, GIF, TIF]
+
+
+matlabCommand :: Configuration -> FigureSpec -> FilePath -> Text
+matlabCommand Configuration{..} _ fp = [st|#{matlabExe} -batch "run('#{fp}')"|]
+
+
+matlabAvailable :: Configuration -> IO Bool
+matlabAvailable Configuration{..} = commandSuccess [st|#{matlabExe} -h|]
+
+matlabCapture :: FigureSpec -> FilePath -> Script
+matlabCapture FigureSpec{..} fname = [st|
+saveas(gcf, '#{fname}')
+|]
+ src/Text/Pandoc/Filter/Plot/Renderers/Matplotlib.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-|
+Module      : $header$
+Copyright   : (c) Laurent P René de Cotret, 2020
+License     : GNU GPL, version 2 or above
+Maintainer  : laurent.decotret@outlook.com
+Stability   : internal
+Portability : portable
+
+Rendering Matplotlib code blocks.
+
+Note that the MatplotlibM renderer supports two extra arguments:
+    * @tight_bbox=True|False@ : Make plot bounding box tight. Default is False
+    * @transparent=True|False@ : Make plot background transparent (perfect for web pages). Default is False.
+-}
+
+module Text.Pandoc.Filter.Plot.Renderers.Matplotlib (
+      matplotlibSupportedSaveFormats
+    , matplotlibCommand
+    , matplotlibCapture
+    , matplotlibExtraAttrs
+    , matplotlibAvailable
+) where
+
+import           Text.Pandoc.Filter.Plot.Renderers.Prelude
+
+import qualified Data.Map.Strict                           as M
+
+
+matplotlibSupportedSaveFormats :: [SaveFormat]
+matplotlibSupportedSaveFormats = [PNG, PDF, SVG, JPG, EPS, GIF, TIF]
+
+
+matplotlibCommand :: Configuration -> FigureSpec -> FilePath -> Text
+matplotlibCommand Configuration{..} _ fp = [st|#{matplotlibExe} #{fp}|]
+
+
+matplotlibCapture :: FigureSpec -> FilePath -> Script
+matplotlibCapture FigureSpec{..} fname = [st|
+import matplotlib.pyplot as plt
+plt.savefig(r"#{fname}", dpi=#{dpi}, transparent=#{transparent}, bbox_inches=#{tightBox})
+|]
+    where attrs        = M.fromList extraAttrs
+          tight_       = readBool $ M.findWithDefault "False" "tight"  attrs
+          transparent_ = readBool $ M.findWithDefault "False" "transparent" attrs
+          tightBox     = if tight_ then ("'tight'"::Text) else ("None"::Text)
+          transparent  = if transparent_ then ("True"::Text) else ("False"::Text)
+
+
+matplotlibExtraAttrs :: M.Map Text Text -> (M.Map Text Text)
+matplotlibExtraAttrs kv = M.filterWithKey (\k _ -> k `elem` ["tight_bbox", "transparent"]) kv
+
+
+matplotlibAvailable :: Configuration -> IO Bool
+matplotlibAvailable Configuration{..} = commandSuccess [st|#{matplotlibExe} -c "import matplotlib"|]
+
+
+-- | Flexible boolean parsing
+readBool :: Text -> Bool
+readBool s | s `elem` ["True",  "true",  "'True'",  "'true'",  "1"] = True
+           | s `elem` ["False", "false", "'False'", "'false'", "0"] = False
+           | otherwise = error $ unpack $ mconcat ["Could not parse '", s, "' into a boolean. Please use 'True' or 'False'"]
+ src/Text/Pandoc/Filter/Plot/Renderers/Octave.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-|
+Module      : $header$
+Copyright   : (c) Laurent P René de Cotret, 2020
+License     : GNU GPL, version 2 or above
+Maintainer  : laurent.decotret@outlook.com
+Stability   : internal
+Portability : portable
+
+Rendering Octave plots code blocks
+-}
+
+module Text.Pandoc.Filter.Plot.Renderers.Octave (
+      octaveSupportedSaveFormats
+    , octaveCommand
+    , octaveCapture
+    , octaveAvailable
+) where
+
+import           Text.Pandoc.Filter.Plot.Renderers.Prelude
+
+
+octaveSupportedSaveFormats :: [SaveFormat]
+octaveSupportedSaveFormats = [PNG, PDF, SVG, JPG, EPS, GIF, TIF]
+
+
+octaveCommand :: Configuration -> FigureSpec -> FilePath -> Text
+octaveCommand Configuration{..} _ fp = [st|#{octaveExe} --no-gui --no-window-system #{fp}|]
+
+
+octaveAvailable :: Configuration -> IO Bool
+octaveAvailable Configuration{..} = commandSuccess [st|#{octaveExe} -h|]
+
+
+octaveCapture :: FigureSpec -> FilePath -> Script
+octaveCapture FigureSpec{..} fname = [st|
+saveas(gcf, '#{fname}')
+|]
+ src/Text/Pandoc/Filter/Plot/Renderers/Plotly.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes       #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-|
+Module      : $header$
+Copyright   : (c) Laurent P René de Cotret, 2020
+License     : GNU GPL, version 2 or above
+Maintainer  : laurent.decotret@outlook.com
+Stability   : internal
+Portability : portable
+
+Rendering Plotly code blocks
+-}
+
+module Text.Pandoc.Filter.Plot.Renderers.Plotly (
+      plotlyPythonSupportedSaveFormats
+    , plotlyPythonCommand
+    , plotlyPythonCapture
+    , plotlyPythonAvailable
+) where
+
+import           Text.Pandoc.Filter.Plot.Renderers.Prelude
+
+
+plotlyPythonSupportedSaveFormats :: [SaveFormat]
+plotlyPythonSupportedSaveFormats = [PNG, JPG, WEBP, PDF, SVG, EPS]
+
+
+plotlyPythonCommand :: Configuration -> FigureSpec -> FilePath -> Text
+plotlyPythonCommand Configuration{..} _ fp = [st|#{plotlyPythonExe} #{fp}|]
+
+
+plotlyPythonAvailable :: Configuration -> IO Bool
+plotlyPythonAvailable Configuration{..} = commandSuccess [st|#{plotlyPythonExe} -c "import plotly.graph_objects"|]
+
+
+plotlyPythonCapture :: FigureSpec -> FilePath -> Script
+plotlyPythonCapture _ fname = [st|
+import plotly.graph_objects as go
+__current_plotly_figure = next(obj for obj in globals().values() if type(obj) == go.Figure)
+__current_plotly_figure.write_image(r"#{fname}")
+|]
+ src/Text/Pandoc/Filter/Plot/Renderers/Prelude.hs view
@@ -0,0 +1,42 @@+
+{-|
+Module      : $header$
+Copyright   : (c) Laurent P René de Cotret, 2020
+License     : GNU GPL, version 2 or above
+Maintainer  : laurent.decotret@outlook.com
+Stability   : internal
+Portability : portable
+
+Prelude for renderers, containing some helpful utilities.
+-}
+
+module Text.Pandoc.Filter.Plot.Renderers.Prelude (
+
+      module Prelude
+    , module Text.Pandoc.Filter.Plot.Types
+    , Text
+    , st
+    , unpack
+    , commandSuccess
+) where
+
+import           Data.Text                     (Text, unpack)
+import           System.Exit                   (ExitCode(..))
+import           System.Process.Typed          (runProcess, shell, 
+                                                setStdout, setStderr, 
+                                                nullStream)
+import           Text.Shakespeare.Text         (st)
+
+
+import           Text.Pandoc.Filter.Plot.Types
+
+
+-- | Check that the supplied command results in
+-- an exit code of 0 (i.e. no errors)
+commandSuccess :: Text -> IO Bool
+commandSuccess s = do
+    ec <- runProcess 
+            $ setStdout nullStream 
+            $ setStderr nullStream 
+            $ shell (unpack s)
+    return $ ec == ExitSuccess
+ src/Text/Pandoc/Filter/Plot/Scripting.hs view
@@ -0,0 +1,141 @@+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE RecordWildCards       #-}
+{-|
+Module      : $header$
+Copyright   : (c) Laurent P René de Cotret, 2020
+License     : GNU GPL, version 2 or above
+Maintainer  : laurent.decotret@outlook.com
+Stability   : internal
+Portability : portable
+
+Scripting
+-}
+
+module Text.Pandoc.Filter.Plot.Scripting
+    ( ScriptResult(..)
+    , runTempScript
+    , runScriptIfNecessary
+    , toImage
+    ) where
+
+import           Control.Monad.Reader
+
+import           Data.Hashable                     (hash)
+import           Data.Maybe                        (fromMaybe)
+import           Data.Monoid                       ((<>))
+import qualified Data.Text                         as T
+import qualified Data.Text.IO                      as T
+
+import           System.Directory                  (createDirectoryIfMissing,
+                                                    doesFileExist)
+import           System.Exit                       (ExitCode (..))
+import           System.FilePath                   (FilePath, addExtension,
+                                                    normalise, replaceExtension,
+                                                    takeDirectory, (</>))
+import           System.IO.Temp                    (getCanonicalTemporaryDirectory)
+import           System.Process.Typed              (runProcess, shell, setStdout, nullStream)
+
+import           Text.Pandoc.Builder               (fromList, imageWith, link,
+                                                    para, toList)
+import           Text.Pandoc.Definition            (Block (..))
+
+import           Text.Pandoc.Filter.Plot.Parse     (captionReader)
+import           Text.Pandoc.Filter.Plot.Renderers
+import           Text.Pandoc.Filter.Plot.Types
+
+
+-- | Possible result of running a script
+data ScriptResult
+    = ScriptSuccess
+    | ScriptChecksFailed String -- Message
+    | ScriptFailure String Int  -- Command and exit code
+
+-- Run script as described by the spec, only if necessary
+runScriptIfNecessary :: FigureSpec -> PlotM ScriptResult
+runScriptIfNecessary spec = do
+    liftIO $ createDirectoryIfMissing True . takeDirectory $ figurePath spec
+
+    fileAlreadyExists <- liftIO . doesFileExist $ figurePath spec
+    result <- if fileAlreadyExists
+                then return ScriptSuccess
+                else runTempScript spec
+
+    case result of
+        ScriptSuccess      -> liftIO $ T.writeFile (sourceCodePath spec) (script spec) >> return ScriptSuccess
+        ScriptFailure cmd code -> return $ ScriptFailure cmd code
+        ScriptChecksFailed msg -> return $ ScriptChecksFailed msg
+
+
+-- Run script as described by the spec
+-- Checks are performed, according to the renderer
+-- Note that stdout from the script is suppressed, but not
+-- stderr.
+runTempScript :: FigureSpec -> PlotM ScriptResult
+runTempScript spec@FigureSpec{..} = do
+    tk <- asks toolkit
+    conf <- asks config
+    let checks = scriptChecks tk
+        checkResult = mconcat $ checks <*> [script]
+    case checkResult of
+        CheckFailed msg -> return $ ScriptChecksFailed msg
+        CheckPassed -> do
+            -- We involve the script hash as a temporary filename
+            -- so that there is never any collision
+            scriptPath <- tempScriptPath spec
+            let captureFragment = (capture tk) spec (figurePath spec)
+                scriptWithCapture = mconcat [script, "\n", captureFragment]
+            liftIO $ T.writeFile scriptPath scriptWithCapture
+            let command_ = T.unpack $ command tk conf spec scriptPath
+
+            ec <- liftIO 
+                    $ runProcess 
+                    $ setStdout nullStream
+                    $ shell command_
+            case ec of
+                ExitSuccess      -> return   ScriptSuccess
+                ExitFailure code -> return $ ScriptFailure command_ code
+
+
+-- | Convert a @FigureSpec@ to a Pandoc block component.
+-- Note that the script to generate figure files must still
+-- be run in another function.
+toImage :: FigureSpec -> Block
+toImage spec = head . toList $ para $ imageWith attrs' (T.pack target') "fig:" caption'
+    -- To render images as figures with captions, the target title
+    -- must be "fig:"
+    -- Janky? yes
+    where
+        attrs'       = blockAttrs spec
+        target'      = figurePath spec
+        withSource'  = withSource spec
+        srcLink      = link (T.pack $ replaceExtension target' ".txt") mempty "Source code"
+        captionText  = fromList $ fromMaybe mempty (captionReader $ caption spec)
+        captionLinks = mconcat [" (", srcLink, ")"]
+        caption'     = if withSource' then captionText <> captionLinks else captionText
+
+
+-- | Determine the temp script path from Figure specifications
+-- Note that for certain renderers, the appropriate file extension
+-- is important.
+tempScriptPath :: FigureSpec -> PlotM FilePath
+tempScriptPath FigureSpec{..} = do
+    tk <- asks toolkit
+    -- Note that matlab will refuse to process files that don't start with
+    -- a letter... so we append the renderer name
+    let ext = scriptExtension tk
+        hashedPath = "pandocplot" <> (show . abs . hash $ script) <> ext
+    liftIO $ (</> hashedPath) <$> getCanonicalTemporaryDirectory
+
+
+-- | Determine the path to the source code that generated the figure.
+sourceCodePath :: FigureSpec -> FilePath
+sourceCodePath = normalise . flip replaceExtension ".txt" . figurePath
+
+
+-- | Determine the path a figure should have.
+figurePath :: FigureSpec -> FilePath
+figurePath spec = normalise $ directory spec </> stem spec
+  where
+    stem = flip addExtension ext . show . hash
+    ext  = extension . saveFormat $ spec
+ src/Text/Pandoc/Filter/Plot/Types.hs view
@@ -0,0 +1,278 @@+{-# LANGUAGE DeriveGeneric     #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+
+{-|
+Module      : $header$
+Copyright   : (c) Laurent P René de Cotret, 2020
+License     : GNU GPL, version 2 or above
+Maintainer  : laurent.decotret@outlook.com
+Stability   : internal
+Portability : portable
+
+This module defines types in use in pandoc-plot
+-}
+
+module Text.Pandoc.Filter.Plot.Types (
+      Toolkit(..)
+    , PlotM
+    , PlotEnv(..)
+    , Configuration(..)
+    , Script
+    , CheckResult(..)
+    , InclusionKey(..)
+    , FigureSpec(..)
+    , SaveFormat(..)
+    , cls
+    , extension
+    , toolkits
+    , inclusionKeys
+    -- Utilities
+    , isWindows
+) where
+
+import           Control.Monad.Reader
+
+import           Data.Char              (toLower)
+import           Data.Default.Class     (Default, def)
+import           Data.Hashable          (Hashable (..))
+import           Data.List              (intersperse)
+import           Data.Semigroup         (Semigroup (..))
+import           Data.String            (IsString (..))
+import           Data.Text              (Text)
+import           Data.Yaml
+
+import           GHC.Generics           (Generic)
+import           System.Info            (os)
+
+import           Text.Pandoc.Definition (Attr)
+
+toolkits :: [Toolkit]
+toolkits = enumFromTo minBound maxBound
+
+-- | Enumeration of supported toolkits
+data Toolkit
+    = Matplotlib
+    | Matlab
+    | PlotlyPython
+    | Mathematica
+    | Octave
+    | GGPlot2
+    deriving (Bounded, Eq, Enum, Generic)
+
+-- | This instance should only be used to display toolkit names
+instance Show Toolkit where
+    show Matplotlib   = "Python/Matplotlib"
+    show Matlab       = "MATLAB"
+    show PlotlyPython = "Python/Plotly"
+    show Mathematica  = "Mathematica"
+    show Octave       = "GNU Octave"
+    show GGPlot2      = "ggplot2"
+
+-- | Class name which will trigger the filter
+cls :: Toolkit -> Text
+cls Matplotlib   = "matplotlib"
+cls Matlab       = "matlabplot"
+cls PlotlyPython = "plotly_python"
+cls Mathematica  = "mathplot"
+cls Octave       = "octaveplot"
+cls GGPlot2      = "ggplot2"
+
+
+type PlotM a = ReaderT PlotEnv IO a
+
+data PlotEnv
+    = PlotEnv { toolkit :: !Toolkit
+              , config  :: !Configuration
+              }
+
+data Configuration = Configuration
+    { defaultDirectory      :: !FilePath   -- ^ The default directory where figures will be saved.
+    , defaultWithSource     :: !Bool       -- ^ The default behavior of whether or not to include links to source code and high-res
+    , defaultDPI            :: !Int        -- ^ The default dots-per-inch value for generated figures. Renderers might ignore this.
+    , defaultSaveFormat     :: !SaveFormat -- ^ The default save format of generated figures.
+    -- Default preambles
+    , matplotlibPreamble    :: !Script
+    , plotlyPythonPreamble  :: !Script
+    , matlabPreamble        :: !Script
+    , mathematicaPreamble   :: !Script
+    , octavePreamble        :: !Script
+    , ggplot2Preamble       :: !Script
+    -- Toolkit executables
+    , matplotlibExe         :: !FilePath
+    , matlabExe             :: !FilePath
+    , plotlyPythonExe       :: !FilePath
+    , mathematicaExe        :: !FilePath
+    , octaveExe             :: !FilePath
+    , ggplot2Exe            :: !FilePath
+    -- Toolkit-specific options
+    , matplotlibTightBBox   :: !Bool
+    , matplotlibTransparent :: !Bool
+    } deriving (Eq, Show)
+
+instance Default Configuration where
+    def = Configuration
+          { defaultDirectory  = "plots/"
+          , defaultWithSource = False
+          , defaultDPI        = 80
+          , defaultSaveFormat = PNG
+          -- toolkit-specific default preambles
+          , matplotlibPreamble  = mempty
+          , plotlyPythonPreamble= mempty
+          , matlabPreamble      = mempty
+          , mathematicaPreamble = mempty
+          , octavePreamble      = mempty
+          , ggplot2Preamble     = mempty
+          -- Toolkit executables
+          -- Default values are executable names as if on the PATH
+          , matplotlibExe       = if isWindows then "python" else "python3"
+          , matlabExe           = "matlab"
+          , plotlyPythonExe     = if isWindows then "python" else "python3"
+          , mathematicaExe      = "math"
+          , octaveExe           = "octave"
+          , ggplot2Exe          = "Rscript"
+          -- Toolkit-specific
+          , matplotlibTightBBox   = False
+          , matplotlibTransparent = False
+          }
+
+type Script = Text
+
+-- | Result of checking scripts for problems
+data CheckResult
+    = CheckPassed
+    | CheckFailed String
+    deriving (Eq)
+
+instance Semigroup CheckResult where
+    (<>) CheckPassed a                         = a
+    (<>) a CheckPassed                         = a
+    (<>) (CheckFailed msg1) (CheckFailed msg2) = CheckFailed (msg1 <> msg2)
+
+instance Monoid CheckResult where
+    mempty = CheckPassed
+
+-- | Description of any possible inclusion key, both in documents
+-- and in configuration files.
+data InclusionKey
+    = DirectoryK
+    | CaptionK
+    | SaveFormatK
+    | WithSourceK
+    | PreambleK
+    | DpiK
+    | ExecutableK
+    | MatplotlibTightBBoxK
+    | MatplotlibTransparentK
+    | MatplotlibPreambleK
+    | MatplotlibExecutableK
+    | PlotlyPythonPreambleK
+    | PlotlyPythonExecutableK
+    | MatlabPreambleK
+    | MatlabExecutableK
+    | MathematicaPreambleK
+    | MathematicaExecutableK
+    | OctavePreambleK
+    | OctaveExecutableK
+    | GGPlot2PreambleK
+    | GGPlot2ExecutableK
+    deriving (Bounded, Eq, Enum)
+
+-- | Keys that pandoc-plot will look for in code blocks.
+-- These are only exported for testing purposes.
+instance Show InclusionKey where
+    show DirectoryK             = "directory"
+    show CaptionK               = "caption"
+    show SaveFormatK            = "format"
+    show WithSourceK            = "source"
+    show PreambleK              = "preamble"
+    show DpiK                   = "dpi"
+    show ExecutableK            = "executable"
+    show MatplotlibTightBBoxK   = "tight_bbox"
+    show MatplotlibTransparentK = "transparent"
+    show MatplotlibPreambleK    = show PreambleK
+    show PlotlyPythonPreambleK  = show PreambleK
+    show MatlabPreambleK        = show PreambleK
+    show MathematicaPreambleK   = show PreambleK
+    show OctavePreambleK        = show PreambleK
+    show GGPlot2PreambleK       = show PreambleK
+    show MatplotlibExecutableK  = show ExecutableK
+    show MatlabExecutableK      = show ExecutableK
+    show PlotlyPythonExecutableK = show ExecutableK
+    show MathematicaExecutableK = show ExecutableK
+    show OctaveExecutableK      = show ExecutableK
+    show GGPlot2ExecutableK     = show ExecutableK
+
+
+-- | List of all keys related to pandoc-plot that
+-- can be specified in source material.
+inclusionKeys :: [InclusionKey]
+inclusionKeys = enumFromTo (minBound::InclusionKey) maxBound
+
+
+-- | Datatype containing all parameters required to run pandoc-plot.
+--
+-- It is assumed that once a @FigureSpec@ has been created, no configuration
+-- can overload it; hence, a @FigureSpec@ completely encodes a particular figure.
+data FigureSpec = FigureSpec
+    { caption    :: !Text           -- ^ Figure caption.
+    , withSource :: !Bool           -- ^ Append link to source code in caption.
+    , script     :: !Script         -- ^ Source code for the figure.
+    , saveFormat :: !SaveFormat     -- ^ Save format of the figure.
+    , directory  :: !FilePath       -- ^ Directory where to save the file.
+    , dpi        :: !Int            -- ^ Dots-per-inch of figure.
+    , extraAttrs :: ![(Text, Text)] -- ^ Renderer-specific extra attributes.
+    , blockAttrs :: !Attr           -- ^ Attributes not related to @pandoc-plot@ will be propagated.
+    } deriving Generic
+
+instance Hashable FigureSpec -- From Generic
+
+-- | Generated figure file format supported by pandoc-plot.
+-- Note: all formats are supported by Matplotlib, but not all
+-- formats are supported by Plotly
+data SaveFormat
+    = PNG
+    | PDF
+    | SVG
+    | JPG
+    | EPS
+    | GIF
+    | TIF
+    | WEBP
+    deriving (Bounded, Enum, Eq, Show, Generic)
+
+instance Hashable SaveFormat -- From Generic
+
+instance IsString SaveFormat where
+    -- An error is thrown if the save format cannot be parsed. That's OK
+    -- since pandoc-plot is a command-line tool and isn't expected to run
+    -- long.
+    fromString s
+        | s `elem` ["png", "PNG", ".png"] = PNG
+        | s `elem` ["pdf", "PDF", ".pdf"] = PDF
+        | s `elem` ["svg", "SVG", ".svg"] = SVG
+        | s `elem` ["eps", "EPS", ".eps"] = EPS
+        | s `elem` ["gif", "GIF", ".gif"] = GIF
+        | s `elem` ["jpg", "jpeg", "JPG", "JPEG", ".jpg", ".jpeg"] = JPG
+        | s `elem` ["tif", "tiff", "TIF", "TIFF", ".tif", ".tiff"] = TIF
+        | s `elem` ["webp", "WEBP", ".webp"] = WEBP
+        | otherwise = error $
+                mconcat [ s
+                        , " is not one of valid save format : "
+                        , mconcat $ intersperse ", " $ show <$> saveFormats
+                        ]
+        where
+            saveFormats =  (enumFromTo minBound maxBound) :: [SaveFormat]
+
+instance FromJSON SaveFormat -- TODO: test this parsing
+
+instance ToJSON SaveFormat where
+    toJSON = toJSON . extension
+
+-- | Save format file extension
+extension :: SaveFormat -> String
+extension fmt = mconcat [".", fmap toLower . show $ fmt]
+
+
+isWindows :: Bool
+isWindows = os `elem` ["mingw32", "win32", "cygwin32"] -- Aliases taken from cabal's Distribution.System module
+ stack.yaml view
@@ -0,0 +1,76 @@+# This file was automatically generated by 'stack init'
+#
+# Some commonly used options have been documented as comments in this file.
+# For advanced use and comprehensive documentation of the format, please see:
+# https://docs.haskellstack.org/en/stable/yaml_configuration/
+
+# Resolver to choose a 'specific' stackage snapshot or a compiler version.
+# A snapshot resolver dictates the compiler version and the set of packages
+# to be used for project dependencies. For example:
+#
+# resolver: lts-3.5
+# resolver: nightly-2015-09-21
+# resolver: ghc-7.10.2
+#
+# The location of a snapshot can be provided as a file or url. Stack assumes
+# a snapshot provided as a file might change, whereas a url resource does not.
+#
+# resolver: ./custom-snapshot.yaml
+# resolver: https://example.com/snapshots/2018-01-01.yaml
+resolver: lts-14.1
+
+# User packages to be built.
+# Various formats can be used as shown in the example below.
+#
+# packages:
+# - some-directory
+# - https://example.com/foo/bar/baz-0.0.2.tar.gz
+# - location:
+#    git: https://github.com/commercialhaskell/stack.git
+#    comGNU GPL, version 2 or above: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
+# - location: https://github.com/commercialhaskell/stack/comGNU GPL, version 2 or above/e7b331f14bcffb8367cd58fbfc8b40ec7642100a
+#  subdirs:
+#  - auto-update
+#  - wai
+packages:
+- .
+# Dependency packages to be pulled from upstream that are not in the resolver
+# using the same syntax as the packages field.
+# (e.g., acme-missiles-0.3)
+extra-deps:
+- pandoc-2.9
+- pandoc-types-1.20@sha256:8393b1a73b8a6a1f3feaeb3a6592c176461082c3e4d897f1b316b1a58dd84c39,3999
+- texmath-0.12
+- HsYAML-0.2.1.0@sha256:e4677daeba57f7a1e9a709a1f3022fe937336c91513e893166bd1f023f530d68,5311
+- doclayout-0.2.0.1@sha256:0410e40c4fa8e299b4f5fa03d378111b9d0effdf59134c95882a160637887ba8,2063
+- haddock-library-1.8.0@sha256:9dece2cbca827755fdfc30af5a407b0ca30cf29ec1ee85215b38fd8fc23e7421,3723
+- regex-pcre-builtin-0.95.1.1.8.43@sha256:2d671af361adf1776fde182a687bb6da022b1e5e3b0a064ce264289de63564a5,3088
+- regex-base-0.94.0.0@sha256:d514eab2aa9ba4b9d14900ac40cbdea1993372466a6cc6ffeeab59a1975563c0,2166
+- doctemplates-0.8@sha256:a85670fbc199422ff06b3f511c426a94e72d3ee1f2d165bfdfb64ec6bb48bdc3,3109
+- emojis-0.1@sha256:3cd86b552ad71c118a7822128c97054b6cf22bc4ff5b8f7e3eb0b356202aeecd,1907
+- skylighting-0.8.3@sha256:dbd885fc6be993cb3714b67ab1ba32c110dc889cd96d2a70f4dc2f67518ad5d3,9726
+- skylighting-core-0.8.3@sha256:90cf39790b38f77f13a29a7a64a7c50a8cb0442c2c68f5cd7b44c12a32e8dd37,8056
+
+# Override default flag values for local packages and extra-deps
+# flags: {}
+
+# Extra package databases containing global packages
+# extra-package-dbs: []
+
+# Control whether we use the GHC we find on the path
+# system-ghc: true
+#
+# Require a specific version of stack, using version ranges
+# require-stack-version: -any # Default
+# require-stack-version: ">=1.10"
+#
+# Override the architecture used by stack, especially useful on Windows
+# arch: i386
+# arch: x86_64
+#
+# Extra directories used by stack for building
+# extra-include-dirs: [/path/to/dir]
+# extra-lib-dirs: [/path/to/dir]
+#
+# Allow a newer minor version of GHC than the snapshot specifies
+# compiler-check: newer-minor
+ tests/Common.hs view
@@ -0,0 +1,165 @@+{-# LANGUAGE FlexibleContexts  #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Common where
+
+import           Control.Monad                    (unless)
+import           Control.Monad.Reader
+
+import           Data.Default.Class               (def)
+import           Data.List                        (isInfixOf, isSuffixOf)
+import           Data.Monoid                      ((<>))
+import           Data.Text                        (Text, pack, unpack)
+
+import           Test.Tasty
+import           Test.Tasty.HUnit
+
+import           Text.Pandoc.Filter.Plot
+import           Text.Pandoc.Filter.Plot.Internal
+
+import qualified Text.Pandoc.Builder              as B
+import qualified Text.Pandoc.Definition           as B
+import           Text.Pandoc.JSON
+
+import           System.Directory                 (createDirectory,
+                                                   createDirectoryIfMissing,
+                                                   doesDirectoryExist,
+                                                   doesFileExist, listDirectory,
+                                                   removeDirectoryRecursive,
+                                                   removePathForcibly)
+import           System.FilePath                  (takeExtensions, (</>))
+import           System.IO.Temp                   (getCanonicalTemporaryDirectory)
+
+
+-------------------------------------------------------------------------------
+-- Test that plot files and source files are created when the filter is run
+testFileCreation :: Toolkit -> TestTree
+testFileCreation tk =
+    testCase "writes output files in appropriate directory" $ do
+        tempDir <- (</> "test-file-creation") <$> getCanonicalTemporaryDirectory
+        ensureDirectoryExistsAndEmpty tempDir
+
+        let cb = (addDirectory tempDir $ codeBlock tk (trivialContent tk))
+        _ <- (make tk) def cb
+        filesCreated <- length <$> listDirectory tempDir
+        assertEqual "" 2 filesCreated
+
+-------------------------------------------------------------------------------
+-- Test that included files are found within the source
+testFileInclusion :: Toolkit -> TestTree
+testFileInclusion tk =
+    testCase "includes plot inclusions" $ do
+        tempDir <- (</> "test-file-inclusion") <$> getCanonicalTemporaryDirectory
+        ensureDirectoryExistsAndEmpty tempDir
+
+        let cb = (addInclusion (include tk) $
+                    addDirectory tempDir $ codeBlock tk (trivialContent tk))
+        _ <- (make tk) def cb
+        inclusion <- readFile (include tk)
+        sourcePath <- head . filter (isExtensionOf "txt") <$> listDirectory tempDir
+        src <- readFile (tempDir </> sourcePath)
+        assertIsInfix inclusion src
+    where
+        include Matplotlib   = "tests/includes/matplotlib.py"
+        include PlotlyPython = "tests/includes/plotly-python.py"
+        include Matlab       = "tests/includes/matlabplot.m"
+        include Mathematica  = "tests/includes/mathplot.m"
+        include Octave       = "tests/includes/octave.m"
+        include GGPlot2      = "tests/includes/ggplot2.r"
+
+-------------------------------------------------------------------------------
+-- Test that the files are saved in the appropriate format
+testSaveFormat :: Toolkit -> TestTree
+testSaveFormat tk =
+    testCase "saves in the appropriate format" $ do
+        tempDir <- (</> "test-safe-format") <$> getCanonicalTemporaryDirectory
+        ensureDirectoryExistsAndEmpty tempDir
+        let fmt = head (supportedSaveFormats tk)
+            cb = (addSaveFormat fmt $
+                 addDirectory tempDir $ codeBlock tk (trivialContent tk))
+        _ <- (make tk) def cb
+        numberjpgFiles <-
+            length <$> filter (isExtensionOf (extension fmt)) <$>
+            listDirectory tempDir
+        assertEqual "" numberjpgFiles 1
+
+
+
+codeBlock :: Toolkit -> Script -> Block
+codeBlock tk script = CodeBlock (mempty, [cls tk], mempty) script
+
+
+trivialContent :: Toolkit -> Script
+trivialContent Matplotlib   = "import matplotlib.pyplot as plt\n"
+trivialContent PlotlyPython = "import plotly.graph_objects as go; fit = go.Figure()\n"
+trivialContent Matlab       = "figure('visible', 'off')\n"
+trivialContent Mathematica  = "\n"
+trivialContent Octave       = "figure('visible', 'off')\nplot (-10:0.1:10);"
+trivialContent GGPlot2      = "library(ggplot2)\nggplot()\n"
+
+
+addCaption :: String -> Block -> Block
+addCaption caption (CodeBlock (id', cls, attrs) script) =
+    CodeBlock (id', cls, attrs ++ [(tshow CaptionK, pack caption)]) script
+
+
+addDirectory :: FilePath -> Block -> Block
+addDirectory dir (CodeBlock (id', cls, attrs) script) =
+    CodeBlock (id', cls, attrs ++ [(tshow DirectoryK, pack dir)]) script
+
+
+addInclusion :: FilePath -> Block -> Block
+addInclusion inclusionPath (CodeBlock (id', cls, attrs) script) =
+    CodeBlock (id', cls, attrs ++ [(tshow PreambleK, pack inclusionPath)]) script
+
+
+addSaveFormat :: SaveFormat -> Block -> Block
+addSaveFormat saveFormat (CodeBlock (id', cls, attrs) script) =
+    CodeBlock (id', cls, attrs ++ [(tshow SaveFormatK, pack . extension $ saveFormat)]) script
+
+
+addDPI :: Int -> Block -> Block
+addDPI dpi (CodeBlock (id', cls, attrs) script) =
+    CodeBlock (id', cls, attrs ++ [(tshow DpiK, pack . show $ dpi)]) script
+
+
+addWithSource :: Bool -> Block -> Block
+addWithSource yn (CodeBlock (id', cls, attrs) script) =
+    CodeBlock (id', cls, attrs ++ [(tshow WithSourceK, pack . show $ yn)]) script
+
+
+-- | Assert that a file exists
+assertFileExists :: HasCallStack => FilePath -> Assertion
+assertFileExists filepath = do
+    fileExists <- doesFileExist filepath
+    unless fileExists (assertFailure msg)
+  where
+    msg = mconcat ["File ", filepath, " does not exist."]
+
+
+-- | Not available with GHC < 8.4
+-- since this function was added in filepath-1.4.2
+-- but GHC 8.2.2 comes with filepath-1.4.1.2
+isExtensionOf :: String -> FilePath -> Bool
+isExtensionOf ext@('.':_) = isSuffixOf ext . takeExtensions
+isExtensionOf ext         = isSuffixOf ('.':ext) . takeExtensions
+
+
+-- | Assert that the first list is contained,
+-- wholly and intact, anywhere within the second.
+assertIsInfix :: (Eq a, Show a, HasCallStack) => [a] -> [a] -> Assertion
+assertIsInfix xs ys = unless (xs `isInfixOf` ys) (assertFailure msg)
+  where
+    msg = mconcat ["Expected ", show xs, " to be an infix of ", show ys]
+
+-- Ensure a directory is empty but exists.
+ensureDirectoryExistsAndEmpty :: FilePath -> IO ()
+ensureDirectoryExistsAndEmpty dir = do
+    exists <- doesDirectoryExist dir
+    if exists
+        then removePathForcibly dir
+        else return ()
+    createDirectory dir
+
+tshow :: Show a => a -> Text
+tshow = pack . show
+ tests/Main.hs view
@@ -0,0 +1,67 @@+{-# LANGUAGE FlexibleContexts  #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+import           Control.Monad                    (forM_)
+
+import           Data.Default.Class               (Default, def)
+import           Data.Text                        (Text, unpack)
+
+import           Test.Tasty
+import           Test.Tasty.HUnit
+
+import           Common
+import           Text.Pandoc.Filter.Plot.Internal
+
+main :: IO ()
+main = do
+    available <- availableToolkits def
+    unavailable <- unavailableToolkits def
+    forM_ unavailable $ \tk -> do
+        putStrLn $ show tk <> " is not availble. Its tests will be skipped."
+
+    defaultMain $ testGroup "All tests"
+        [ testGroup
+            "Configuration tests"
+            [ testEmptyConfiguration
+            , testExampleConfiguration
+            ]        
+        , testGroup
+            "Toolkit tests"
+            (toolkitSuite <$> available)
+        ]
+
+
+-- | Suite of tests that every renderer should pass
+toolkitSuite :: Toolkit -> TestTree
+toolkitSuite tk =
+    testGroup (show tk) $
+        [ testFileCreation
+        , testFileInclusion
+        , testSaveFormat
+        ] <*> [tk]
+
+
+testEmptyConfiguration :: TestTree
+testEmptyConfiguration = 
+    testCase "empty configuration is correctly parsed to default values" $ do
+        let config = def
+
+        parsedConfig <- configuration "tests/fixtures/.pandoc-plot.yml"
+        assertEqual "" config parsedConfig
+
+
+-- The exampel configuration is build by hand (to add comments)
+-- and it is embedded into the executable. Therefore, we must make sure it 
+-- is correctly parsed (and is therefore valid.)
+testExampleConfiguration :: TestTree
+testExampleConfiguration = 
+    testCase "example configuration is correctly parsed" $ do
+        -- The example config reflects the Windows default
+        -- Therefore, we need to test against the Windows default,
+        -- even on other OSes
+        let config = def { matplotlibExe = "python"
+                         , plotlyPythonExe = "python"
+                         }
+
+        parsedConfig <- configuration "example-config.yml"
+        assertEqual "" config parsedConfig