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