diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22fc5eb..c67e5ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,19 +98,23 @@ jobs: run: | Rscript -e "install.packages('ggplot2', repos='http://cran.rstudio.com/')" - - name: Install Octave, Gnuplot, Graphviz, and PlantUML [Linux] + - name: Install Octave, Gnuplot, Graphviz, PlantUML, and Asymptote [Linux] if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get --quiet --yes install octave sudo apt-get --quiet --yes install gnuplot sudo apt-get --quiet --yes install graphviz - + sudo apt-get --quiet --yes install asymptote sudo apt-get --quiet --yes install plantuml + echo $(dot -version v .:? asKey PreambleK <*> v .:? asKey ExecutableK .!= d2Exe defaultConfiguration <*> v .:? asKey CommandLineArgsK .!= d2CmdArgs defaultConfiguration parseJSON _ = fail $ mconcat ["Could not parse ", show SageMath, " configuration."] +instance FromJSON AsyPrecursor where + parseJSON (Object v) = AsyPrecursor <$> v .:? asKey PreambleK <*> v .:? asKey ExecutableK .!= asyExe defaultConfiguration <*> v .:? asKey CommandLineArgsK .!= asyCmdArgs defaultConfiguration + parseJSON _ = fail $ mconcat ["Could not parse ", show Asymptote, " configuration."] + toolkitAsKey :: Toolkit -> Key toolkitAsKey = fromString . unpack . cls @@ -328,6 +339,7 @@ instance FromJSON ConfigPrecursor where _plantumlPrec <- v .:? toolkitAsKey PlantUML .!= _plantumlPrec defaultConfigPrecursor _sagemathPrec <- v .:? toolkitAsKey SageMath .!= _sagemathPrec defaultConfigPrecursor _d2Prec <- v .:? toolkitAsKey D2 .!= _d2Prec defaultConfigPrecursor + _asyPrec <- v .:? toolkitAsKey Asymptote .!= _asyPrec defaultConfigPrecursor return $ ConfigPrecursor {..} parseJSON _ = fail "Could not parse configuration." @@ -363,6 +375,7 @@ renderConfig ConfigPrecursor {..} = do plantumlExe = _plantumlExe _plantumlPrec sagemathExe = _sagemathExe _sagemathPrec d2Exe = _d2Exe _d2Prec + asyExe = _asyExe _asyPrec matplotlibCmdArgs = _matplotlibCmdArgs _matplotlibPrec matlabCmdArgs = _matlabCmdArgs _matlabPrec @@ -378,6 +391,7 @@ renderConfig ConfigPrecursor {..} = do plantumlCmdArgs = _plantumlCmdArgs _plantumlPrec sagemathCmdArgs = _sagemathCmdArgs _sagemathPrec d2CmdArgs = _d2CmdArgs _d2Prec + asyCmdArgs = _asyCmdArgs _asyPrec matplotlibPreamble <- readPreamble (_matplotlibPreamble _matplotlibPrec) matlabPreamble <- readPreamble (_matlabPreamble _matlabPrec) @@ -393,6 +407,7 @@ renderConfig ConfigPrecursor {..} = do plantumlPreamble <- readPreamble (_plantumlPreamble _plantumlPrec) sagemathPreamble <- readPreamble (_sagemathPreamble _sagemathPrec) d2Preamble <- readPreamble (_d2Preamble _d2Prec) + asyPreamble <- readPreamble (_asyPreamble _asyPrec) return Configuration {..} where diff --git a/src/Text/Pandoc/Filter/Plot/Monad.hs b/src/Text/Pandoc/Filter/Plot/Monad.hs index 66f87a7..ed5b607 100644 --- a/src/Text/Pandoc/Filter/Plot/Monad.hs +++ b/src/Text/Pandoc/Filter/Plot/Monad.hs @@ -283,6 +283,7 @@ executable tk = exeSelector tk <&> exeFromPath exeSelector PlantUML = asksConfig plantumlExe exeSelector SageMath = asksConfig sagemathExe exeSelector D2 = asksConfig d2Exe + exeSelector Asymptote = asksConfig asyExe -- | The @Configuration@ type holds the default values to use -- when running pandoc-plot. These values can be overridden in code blocks. @@ -353,6 +354,8 @@ data Configuration = Configuration sagemathPreamble :: !Script, -- | The default preamble script for the d2 toolkit. d2Preamble :: !Script, + -- | The default preamble script for the Asymptote toolkit. + asyPreamble :: !Script, -- | The executable to use to generate figures using the matplotlib toolkit. matplotlibExe :: !FilePath, -- | The executable to use to generate figures using the MATLAB toolkit. @@ -381,6 +384,8 @@ data Configuration = Configuration sagemathExe :: !FilePath, -- | The executable to use to generate figures using d2. d2Exe :: !FilePath, + -- | The executable to use to generate figures using Asymptote + asyExe :: !FilePath, -- | Command-line arguments to pass to the Python interpreter for the Matplotlib toolkit matplotlibCmdArgs :: !Text, -- | Command-line arguments to pass to the interpreter for the MATLAB toolkit. @@ -409,6 +414,8 @@ data Configuration = Configuration sagemathCmdArgs :: !Text, -- | Command-line arguments to pass to the interpreter for the d2 toolkit. d2CmdArgs :: !Text, + -- | Command-line arguments to pass to the interpreter for the Asymptote toolkit. + asyCmdArgs :: !Text, -- | Whether or not to make Matplotlib figures tight by default. matplotlibTightBBox :: !Bool, -- | Whether or not to make Matplotlib figures transparent by default. diff --git a/src/Text/Pandoc/Filter/Plot/Monad/Types.hs b/src/Text/Pandoc/Filter/Plot/Monad/Types.hs index f06b300..003c827 100644 --- a/src/Text/Pandoc/Filter/Plot/Monad/Types.hs +++ b/src/Text/Pandoc/Filter/Plot/Monad/Types.hs @@ -62,6 +62,7 @@ data Toolkit | PlantUML | SageMath | D2 + | Asymptote deriving (Bounded, Eq, Enum, Generic, Ord) -- | This instance should only be used to display toolkit names @@ -80,6 +81,7 @@ instance Show Toolkit where show PlantUML = "PlantUML" show SageMath = "SageMath" show D2 = "D2" + show Asymptote = "Asymptote" -- | Class name which will trigger the filter cls :: Toolkit -> Text @@ -97,6 +99,7 @@ cls Plotsjl = "plotsjl" cls PlantUML = "plantuml" cls SageMath = "sageplot" cls D2 = "d2" +cls Asymptote = "asy" -- | Executable program, and sometimes the directory where it can be found. data Executable diff --git a/src/Text/Pandoc/Filter/Plot/Renderers.hs b/src/Text/Pandoc/Filter/Plot/Renderers.hs index e727c10..5fca78b 100644 --- a/src/Text/Pandoc/Filter/Plot/Renderers.hs +++ b/src/Text/Pandoc/Filter/Plot/Renderers.hs @@ -95,7 +95,10 @@ import Text.Pandoc.Filter.Plot.Renderers.SageMath ( sagemath, sagemathSupportedSaveFormats, ) - +import Text.Pandoc.Filter.Plot.Renderers.Asymptote + ( asymptote, + asymptoteSupportedSaveFormats, + ) -- | Get the renderer associated with a toolkit. -- If the renderer has not been used before, -- initialize it and store where it is. It will be re-used. @@ -114,6 +117,7 @@ renderer Plotsjl = plotsjl renderer PlantUML = plantuml renderer SageMath = sagemath renderer D2 = d2 +renderer Asymptote = asymptote -- | Save formats supported by this renderer. supportedSaveFormats :: Toolkit -> [SaveFormat] @@ -131,6 +135,7 @@ supportedSaveFormats Plotsjl = plotsjlSupportedSaveFormats supportedSaveFormats PlantUML = plantumlSupportedSaveFormats supportedSaveFormats SageMath = sagemathSupportedSaveFormats supportedSaveFormats D2 = d2SupportedSaveFormats +supportedSaveFormats Asymptote = asymptoteSupportedSaveFormats -- | The function that maps from configuration to the preamble. preambleSelector :: Toolkit -> (Configuration -> Script) @@ -148,6 +153,7 @@ preambleSelector Plotsjl = plotsjlPreamble preambleSelector PlantUML = plantumlPreamble preambleSelector SageMath = sagemathPreamble preambleSelector D2 = d2Preamble +preambleSelector Asymptote = asyPreamble -- | Parse code block headers for extra attributes that are specific -- to this renderer. By default, no extra attributes are parsed. diff --git a/src/Text/Pandoc/Filter/Plot/Renderers/Asymptote.hs b/src/Text/Pandoc/Filter/Plot/Renderers/Asymptote.hs new file mode 100644 index 0000000..6356054 --- /dev/null +++ b/src/Text/Pandoc/Filter/Plot/Renderers/Asymptote.hs @@ -0,0 +1,50 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE NoImplicitPrelude #-} + +-- | +-- Module : $header$ +-- Copyright : (c) Laurent P René de Cotret, 2019 - present +-- License : GNU GPL, version 2 or above +-- Maintainer : laurent.decotret@outlook.com +-- Stability : internal +-- Portability : portable +-- +-- Rendering Asymptote plots code blocks +module Text.Pandoc.Filter.Plot.Renderers.Asymptote + ( asymptote, + asymptoteSupportedSaveFormats, + ) +where + +import Text.Pandoc.Filter.Plot.Renderers.Prelude +import Data.Char(toLower) + +asymptote :: PlotM Renderer +asymptote = do + cmdargs <- asksConfig asyCmdArgs + return + $ Renderer + { rendererToolkit = Asymptote, + rendererCapture = asymptoteCapture, + rendererCommand = asymptoteCommand cmdargs, + rendererAvailability = CommandSuccess $ \exe -> [st|#{pathToExe exe} -environment|], + rendererSupportedSaveFormats = asymptoteSupportedSaveFormats, + rendererChecks = mempty, + rendererLanguage = "asy", + rendererComment = mappend "// ", + rendererScriptExtension = ".asy" + } + +asymptoteSupportedSaveFormats :: [SaveFormat] +asymptoteSupportedSaveFormats = [PDF, EPS, PNG] + +asymptoteCommand :: Text -> OutputSpec -> Text +asymptoteCommand cmdArgs OutputSpec {..} = + [st|#{pathToExe oExecutable} #{cmdArgs} -f #{toLower <$> show (saveFormat oFigureSpec)} -o "#{oFigurePath}" "#{oScriptPath}"|] + +-- Asymptote export is entirely based on command-line arguments +-- so there is no need to modify the script itself. +asymptoteCapture :: FigureSpec -> FilePath -> Script +asymptoteCapture FigureSpec {..} _ = script diff --git a/src/Text/Pandoc/Filter/Plot/Renderers/GGPlot2.hs b/src/Text/Pandoc/Filter/Plot/Renderers/GGPlot2.hs index b2855bc..2039aa1 100644 --- a/src/Text/Pandoc/Filter/Plot/Renderers/GGPlot2.hs +++ b/src/Text/Pandoc/Filter/Plot/Renderers/GGPlot2.hs @@ -38,7 +38,7 @@ ggplot2 = do } ggplot2SupportedSaveFormats :: [SaveFormat] -ggplot2SupportedSaveFormats = [PNG, PDF, SVG, JPG, EPS, TIF] +ggplot2SupportedSaveFormats = [PNG, PDF, SVG, JPG, EPS] ggplot2Command :: Text -> OutputSpec -> Text ggplot2Command cmdargs OutputSpec {..} = [st|#{pathToExe oExecutable} #{cmdargs} "#{oScriptPath}"|] diff --git a/src/Text/Pandoc/Filter/Plot/Renderers/Matplotlib.hs b/src/Text/Pandoc/Filter/Plot/Renderers/Matplotlib.hs index ce74429..760e55b 100644 --- a/src/Text/Pandoc/Filter/Plot/Renderers/Matplotlib.hs +++ b/src/Text/Pandoc/Filter/Plot/Renderers/Matplotlib.hs @@ -44,7 +44,7 @@ matplotlib = do } matplotlibSupportedSaveFormats :: [SaveFormat] -matplotlibSupportedSaveFormats = [PNG, PDF, SVG, JPG, EPS, GIF, TIF] +matplotlibSupportedSaveFormats = [PNG, PDF, SVG, JPG, EPS, TIF] matplotlibCommand :: Text -> OutputSpec -> Text matplotlibCommand cmdargs OutputSpec {..} = [st|#{pathToExe oExecutable} #{cmdargs} "#{oScriptPath}"|] diff --git a/tests/Common.hs b/tests/Common.hs index 6d32a7c..3a14e80 100644 --- a/tests/Common.hs +++ b/tests/Common.hs @@ -3,8 +3,8 @@ module Common where -import Control.Monad (unless, when) -import Data.List (isInfixOf, isSuffixOf, (!!)) +import Control.Monad (unless, when, forM) +import Data.List (isInfixOf, isSuffixOf, (!!), (\\)) import Data.Monoid ((<>)) import qualified Data.Set as S import Data.String (fromString) @@ -33,7 +33,8 @@ defaultTestConfig :: Configuration defaultTestConfig = defaultConfiguration { logVerbosity = Silent, - logSink = StdErr + logSink = StdErr, + defaultSaveFormat = PNG } ------------------------------------------------------------------------------- @@ -121,17 +122,35 @@ testFileInclusion tk = include PlantUML = "tests/includes/plantuml.txt" include SageMath = "tests/includes/sagemath.sage" include D2 = "tests/includes/d2-dd.d2" + include Asymptote = "tests/includes/asymptote.asy" + +------------------------------------------------------------------------------- +-- Tests that the files are saved in all the advertised formats +testAllSaveFormats :: Toolkit -> TestTree +-- Correct formats unsupported on CI. +-- TODO: change when CI support improves +testAllSaveFormats tk@Graphviz = + testGroup "advertised save formats that work on CI" + (testSaveFormat tk <$> (supportedSaveFormats tk \\ [WEBP])) +testAllSaveFormats tk@Matlab = + testGroup "advertised save formats that work on CI" + (testSaveFormat tk <$> (supportedSaveFormats tk \\ [SVG])) +testAllSaveFormats tk@GGPlot2 = + testGroup "advertised save formats that work on CI" + (testSaveFormat tk <$> (supportedSaveFormats tk \\ [SVG])) +-- All other formats: +testAllSaveFormats tk = + testGroup "advertised output formats" (testSaveFormat tk <$> supportedSaveFormats tk) ------------------------------------------------------------------------------- -- Test that the files are saved in the appropriate format -testSaveFormat :: Toolkit -> TestTree -testSaveFormat tk = - testCase "saves in the appropriate format" $ do +testSaveFormat :: Toolkit -> SaveFormat -> TestTree +testSaveFormat tk fmt = + testCase ("saves in the appropriate format (" <> show fmt <> ")") $ do let postfix = unpack . cls $ tk tempDir <- ( "test-safe-format-" <> postfix) <$> getTemporaryDirectory ensureDirectoryExistsAndEmpty tempDir - let fmt = head (supportedSaveFormats tk) - cb = + let cb = ( addSaveFormat fmt $ addDirectory tempDir $ codeBlock tk (trivialContent tk) @@ -403,6 +422,7 @@ trivialContent Plotsjl = "using Plots; x = 1:10; y = rand(10); plot(x, y);" trivialContent PlantUML = "@startuml\nAlice -> Bob: test\n@enduml" trivialContent SageMath = "G = plot(sin, 1, 10)" trivialContent D2 = "x -> y -> z" +trivialContent Asymptote = "draw((0,0)--(1,0));" addCaption :: String -> Block -> Block addCaption caption (CodeBlock (id', cls, attrs) script) = diff --git a/tests/Main.hs b/tests/Main.hs index 4b66177..09abdd8 100644 --- a/tests/Main.hs +++ b/tests/Main.hs @@ -51,7 +51,7 @@ toolkitSuite tk = testFileCreationPathWithSpaces, testNestedCodeBlocks, testFileInclusion, - testSaveFormat, + testAllSaveFormats, testSaveFormatIncompatibility, testWithSource, testSourceLabel, diff --git a/tests/includes/asymptote.asy b/tests/includes/asymptote.asy new file mode 100644 index 0000000..305a9e8 --- /dev/null +++ b/tests/includes/asymptote.asy @@ -0,0 +1 @@ +// This is comment in Asymptote diff --git a/tests/issue55.md b/tests/issue55.md new file mode 100644 index 0000000..8810eff --- /dev/null +++ b/tests/issue55.md @@ -0,0 +1,74 @@ +--- +plot-configuration: tests/fixtures/.verbose-config.yml +--- + +# Example Asymptote plot + +```{.asy} + +// This example was taken from Asymptote's gallery +import graph; + +size(9cm,8cm,IgnoreAspect); +string data="westnile.csv"; + +file in=input(data).line().csv(); + +string[] columnlabel=in; + +real[][] A=in; +A=transpose(A); +real[] number=A[0], survival=A[1]; + +path g=graph(number,survival); +draw(g); + +scale(true); + +xaxis("Initial no.\ of mosquitoes per bird ($S_{M_0}/N_{B_0}$)", + Bottom,LeftTicks); +xaxis(Top); +yaxis("Susceptible bird survival",Left,RightTicks(trailingzero)); +yaxis(Right); + +real a=number[0]; +real b=number[number.length-1]; + +real S1=0.475; +path h1=(a,S1)--(b,S1); +real M1=interp(a,b,intersect(h1,g)[0]); + +real S2=0.9; +path h2=(a,S2)--(b,S2); +real M2=interp(a,b,intersect(h2,g)[0]); + +labelx("$M_1$",M1); +labelx("$M_2$",M2); + +draw((a,S2)--(M2,S2)--(M2,0),Dotted); +draw((a,S1)--(M1,S1)--(M1,0),dashed); + +pen p=fontsize(10pt); + +real y3=0.043; +path reduction=(M1,y3)--(M2,y3); +draw(reduction,Arrow,TrueMargin(0,0.5*(linewidth(Dotted)+linewidth()))); + +arrow(shift(-20,5)*Label(minipage("\flushleft{\begin{itemize}\item[1.] +Estimate proportion of birds surviving at end of season\end{itemize}}",100), + align=NNE), + (M1,S1),NNE,1cm,p,Arrow(NoFill)); + +arrow(shift(-24,5)*Label(minipage("\flushleft{\begin{itemize}\item[2.] +Read off initial mosquito abundance\end{itemize}}",80),align=NNE), + (M1,0),NE,2cm,p,Arrow(NoFill)); + +arrow(shift(20,0)*Label(minipage("\flushleft{\begin{itemize}\item[3.] +Determine desired bird survival for next season\end{itemize}}",90),align=SW), + (M2,S2),SW,arrowlength,p,Arrow(NoFill)); + +arrow(shift(8,-15)*Label(minipage("\flushleft{\begin{itemize}\item[4.] +Calculate required proportional reduction in mosquitoes\end{itemize}}",90), + align=NW), + point(reduction,0.5),NW,1.5cm,p,Arrow(NoFill)); +```