summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurentRDC <>2020-01-21 20:54:00 (GMT)
committerhdiff <hdiff@hdiff.luite.com>2020-01-21 20:54:00 (GMT)
commit336abc44c24c91fc8e972321009f9a76d567b112 (patch)
treead7b2b26a9a1079ad5135a34f1fa0bdb9a7e9336
version 0.1.0.00.1.0.0
-rw-r--r--CHANGELOG.md8
-rw-r--r--LICENSE339
-rw-r--r--README.md300
-rw-r--r--Setup.hs7
-rw-r--r--executable/ExampleConfig.hs31
-rw-r--r--executable/Main.hs148
-rw-r--r--executable/ManPage.hs48
-rw-r--r--pandoc-plot.cabal108
-rw-r--r--src/Text/Pandoc/Filter/Plot.hs108
-rw-r--r--src/Text/Pandoc/Filter/Plot/Configuration.hs174
-rw-r--r--src/Text/Pandoc/Filter/Plot/Internal.hs26
-rw-r--r--src/Text/Pandoc/Filter/Plot/Parse.hs147
-rw-r--r--src/Text/Pandoc/Filter/Plot/Renderers.hs137
-rw-r--r--src/Text/Pandoc/Filter/Plot/Renderers/GGPlot2.hs41
-rw-r--r--src/Text/Pandoc/Filter/Plot/Renderers/Mathematica.hs40
-rw-r--r--src/Text/Pandoc/Filter/Plot/Renderers/Matlab.hs40
-rw-r--r--src/Text/Pandoc/Filter/Plot/Renderers/Matplotlib.hs65
-rw-r--r--src/Text/Pandoc/Filter/Plot/Renderers/Octave.hs41
-rw-r--r--src/Text/Pandoc/Filter/Plot/Renderers/Plotly.hs43
-rw-r--r--src/Text/Pandoc/Filter/Plot/Renderers/Prelude.hs42
-rw-r--r--src/Text/Pandoc/Filter/Plot/Scripting.hs141
-rw-r--r--src/Text/Pandoc/Filter/Plot/Types.hs278
-rw-r--r--stack.yaml76
-rw-r--r--tests/Common.hs165
-rw-r--r--tests/Main.hs67
25 files changed, 2620 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..df14244
--- /dev/null
+++ b/CHANGELOG.md
@@ -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 \ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..89e08fb
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..43673a8
--- /dev/null
+++ b/README.md
@@ -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**.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
index 0000000..c51b623
--- /dev/null
+++ b/Setup.hs
@@ -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
diff --git a/executable/ExampleConfig.hs b/executable/ExampleConfig.hs
new file mode 100644
index 0000000..534728f
--- /dev/null
+++ b/executable/ExampleConfig.hs
@@ -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) \ No newline at end of file
diff --git a/executable/Main.hs b/executable/Main.hs
new file mode 100644
index 0000000..d15bdf5
--- /dev/null
+++ b/executable/Main.hs
@@ -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
+ ] \ No newline at end of file
diff --git a/executable/ManPage.hs b/executable/ManPage.hs
new file mode 100644
index 0000000..3b2f4a4
--- /dev/null
+++ b/executable/ManPage.hs
@@ -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 }
diff --git a/pandoc-plot.cabal b/pandoc-plot.cabal
new file mode 100644
index 0000000..008846a
--- /dev/null
+++ b/pandoc-plot.cabal
@@ -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
+ \ No newline at end of file
diff --git a/src/Text/Pandoc/Filter/Plot.hs b/src/Text/Pandoc/Filter/Plot.hs
new file mode 100644
index 0000000..cab51b9
--- /dev/null
+++ b/src/Text/Pandoc/Filter/Plot.hs
@@ -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 \ No newline at end of file
diff --git a/src/Text/Pandoc/Filter/Plot/Configuration.hs b/src/Text/Pandoc/Filter/Plot/Configuration.hs
new file mode 100644
index 0000000..2fc34b0
--- /dev/null
+++ b/src/Text/Pandoc/Filter/Plot/Configuration.hs
@@ -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 \ No newline at end of file
diff --git a/src/Text/Pandoc/Filter/Plot/Internal.hs b/src/Text/Pandoc/Filter/Plot/Internal.hs
new file mode 100644
index 0000000..c94fd2e
--- /dev/null
+++ b/src/Text/Pandoc/Filter/Plot/Internal.hs
@@ -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 \ No newline at end of file
diff --git a/src/Text/Pandoc/Filter/Plot/Parse.hs b/src/Text/Pandoc/Filter/Plot/Parse.hs
new file mode 100644
index 0000000..5cf29d1
--- /dev/null
+++ b/src/Text/Pandoc/Filter/Plot/Parse.hs
@@ -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'"]
diff --git a/src/Text/Pandoc/Filter/Plot/Renderers.hs b/src/Text/Pandoc/Filter/Plot/Renderers.hs
new file mode 100644
index 0000000..51604df
--- /dev/null
+++ b/src/Text/Pandoc/Filter/Plot/Renderers.hs
@@ -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
diff --git a/src/Text/Pandoc/Filter/Plot/Renderers/GGPlot2.hs b/src/Text/Pandoc/Filter/Plot/Renderers/GGPlot2.hs
new file mode 100644
index 0000000..2d61782
--- /dev/null
+++ b/src/Text/Pandoc/Filter/Plot/Renderers/GGPlot2.hs
@@ -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})
+|]
diff --git a/src/Text/Pandoc/Filter/Plot/Renderers/Mathematica.hs b/src/Text/Pandoc/Filter/Plot/Renderers/Mathematica.hs
new file mode 100644
index 0000000..de22386
--- /dev/null
+++ b/src/Text/Pandoc/Filter/Plot/Renderers/Mathematica.hs
@@ -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]
+|]
diff --git a/src/Text/Pandoc/Filter/Plot/Renderers/Matlab.hs b/src/Text/Pandoc/Filter/Plot/Renderers/Matlab.hs
new file mode 100644
index 0000000..62c00b7
--- /dev/null
+++ b/src/Text/Pandoc/Filter/Plot/Renderers/Matlab.hs
@@ -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}')
+|]
diff --git a/src/Text/Pandoc/Filter/Plot/Renderers/Matplotlib.hs b/src/Text/Pandoc/Filter/Plot/Renderers/Matplotlib.hs
new file mode 100644
index 0000000..11ed1c1
--- /dev/null
+++ b/src/Text/Pandoc/Filter/Plot/Renderers/Matplotlib.hs
@@ -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'"]
diff --git a/src/Text/Pandoc/Filter/Plot/Renderers/Octave.hs b/src/Text/Pandoc/Filter/Plot/Renderers/Octave.hs
new file mode 100644
index 0000000..14af9f5
--- /dev/null
+++ b/src/Text/Pandoc/Filter/Plot/Renderers/Octave.hs
@@ -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}')
+|]
diff --git a/src/Text/Pandoc/Filter/Plot/Renderers/Plotly.hs b/src/Text/Pandoc/Filter/Plot/Renderers/Plotly.hs
new file mode 100644
index 0000000..50999d5
--- /dev/null
+++ b/src/Text/Pandoc/Filter/Plot/Renderers/Plotly.hs
@@ -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}")
+|]
diff --git a/src/Text/Pandoc/Filter/Plot/Renderers/Prelude.hs b/src/Text/Pandoc/Filter/Plot/Renderers/Prelude.hs
new file mode 100644
index 0000000..38c1ce0
--- /dev/null
+++ b/src/Text/Pandoc/Filter/Plot/Renderers/Prelude.hs
@@ -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 \ No newline at end of file
diff --git a/src/Text/Pandoc/Filter/Plot/Scripting.hs b/src/Text/Pandoc/Filter/Plot/Scripting.hs
new file mode 100644
index 0000000..9a089cb
--- /dev/null
+++ b/src/Text/Pandoc/Filter/Plot/Scripting.hs
@@ -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
diff --git a/src/Text/Pandoc/Filter/Plot/Types.hs b/src/Text/Pandoc/Filter/Plot/Types.hs
new file mode 100644
index 0000000..78b3660
--- /dev/null
+++ b/src/Text/Pandoc/Filter/Plot/Types.hs
@@ -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 \ No newline at end of file
diff --git a/stack.yaml b/stack.yaml
new file mode 100644
index 0000000..e31b05e
--- /dev/null
+++ b/stack.yaml
@@ -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
diff --git a/tests/Common.hs b/tests/Common.hs
new file mode 100644
index 0000000..c1741e3
--- /dev/null
+++ b/tests/Common.hs
@@ -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
diff --git a/tests/Main.hs b/tests/Main.hs
new file mode 100644
index 0000000..5d105b7
--- /dev/null
+++ b/tests/Main.hs
@@ -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 \ No newline at end of file