From 8ddc2fc79a45283e7b90f59e9a7763e877d4c044 Mon Sep 17 00:00:00 2001 From: John MacFarlane <jgm@berkeley.edu> Date: Tue, 16 Aug 2022 16:27:31 -0700 Subject: [PATCH] Integrate server into main pandoc. - Remove server flag. - Remove pandoc-server executable. - Add Text.Pandoc.Server as exposed module. [API change] - Re-use Opt (and our existing FromJSON instance) for Params. - Document. --- .github/workflows/ci.yml | 2 +- .github/workflows/release-candidate.yml | 9 +- Makefile | 2 +- app/pandoc.hs | 15 +- {server => doc}/pandoc-server.md | 334 ++++++++++++---------- linux/{pandoc.control.in => control.in} | 0 linux/make_artifacts.sh | 52 ++-- linux/pandoc-server.control.in | 9 - pandoc.cabal | 38 +-- server/Main.hs | 54 ---- server/PandocServer.hs | 301 -------------------- src/Text/Pandoc/Server.hs | 357 ++++++++++++++++++++++++ stack.yaml | 1 - 13 files changed, 604 insertions(+), 570 deletions(-) rename {server => doc}/pandoc-server.md (73%) rename linux/{pandoc.control.in => control.in} (100%) delete mode 100644 linux/pandoc-server.control.in delete mode 100644 server/Main.hs delete mode 100644 server/PandocServer.hs create mode 100644 src/Text/Pandoc/Server.hs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cdf4345d5..fe65baa6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: testopts: '--test-option=--hide-successes --test-option=--ansi-tricks=false' - ghc: '8.10.7' cabal: '3.2' - cabalopts: '-fserver' + cabalopts: '' testopts: '--test-option=--hide-successes --test-option=--ansi-tricks=false' - ghc: '9.0.2' cabal: '3.4' diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 1c5d764e1..c536e5a74 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -96,7 +96,7 @@ jobs: run: | stack --no-terminal setup stack --no-terminal update - stack --no-terminal install + stack --no-terminal install --ghc-options='-j4 +RTS -A256m -RTS -split-sections' - name: Create artifacts run: | @@ -112,8 +112,13 @@ jobs: mkdir -p ${DEST}/bin mkdir -p ${DEST}/share/man/man1 cp ~/.local/bin/pandoc ${DEST}/bin/ - strip ${DEST}/bin/pandoc + SRCDIR=$(pwd) + cd ${DEST}/bin + strip pandoc + ln -s pandoc pandoc-server + cd ${SRCDIR} cp man/pandoc.1 ${DEST}/share/man/man1/pandoc.1 + cp man/pandoc-server.1 ${DEST}/share/man/man1/pandoc-server.1 ~/.local/bin/pandoc -s COPYING.md -Vpagetitle=License -o ${RESOURCES}/license.html chown -R $ME:staff ${ROOT} sed -e "s/PANDOCVERSION/${VERSION}/" macos/distribution.xml.in > ${ARTIFACTS}/distribution.xml diff --git a/Makefile b/Makefile index df2d9cc44..fbd3c0eb6 100644 --- a/Makefile +++ b/Makefile @@ -102,7 +102,7 @@ man/pandoc.1: MANUAL.txt man/pandoc.1.before man/pandoc.1.after --variable footer="pandoc $(version)" \ -o $@ -man/pandoc-server.1: server/pandoc-server.md +man/pandoc-server.1: doc/pandoc-server.md pandoc $< -f markdown -t man -s \ --lua-filter man/manfilter.lua \ --variable footer="pandoc-server $(version)" \ diff --git a/app/pandoc.hs b/app/pandoc.hs index 24c7c5adc..753bea399 100644 --- a/app/pandoc.hs +++ b/app/pandoc.hs @@ -14,7 +14,18 @@ module Main where import qualified Control.Exception as E import Text.Pandoc.App (convertWithOpts, defaultOpts, options, parseOptions) import Text.Pandoc.Error (handleError) +import Text.Pandoc.Server (ServerOpts(..), parseServerOpts, app) +import System.Environment (getProgName) +import qualified Network.Wai.Handler.CGI as CGI +import qualified Network.Wai.Handler.Warp as Warp +import Network.Wai.Middleware.Timeout (timeout) main :: IO () -main = E.catch (parseOptions options defaultOpts >>= convertWithOpts) - (handleError . Left) +main = E.handle (handleError . Left) $ do + prg <- getProgName + case prg of + "pandoc-server.cgi" -> CGI.run (timeout 2 app) + "pandoc-server" -> do + sopts <- parseServerOpts + Warp.run (serverPort sopts) (timeout (serverTimeout sopts) app) + _ -> parseOptions options defaultOpts >>= convertWithOpts diff --git a/server/pandoc-server.md b/doc/pandoc-server.md similarity index 73% rename from server/pandoc-server.md rename to doc/pandoc-server.md index e22063fa8..b5c68d564 100644 --- a/server/pandoc-server.md +++ b/doc/pandoc-server.md @@ -12,16 +12,17 @@ date: August 15, 2022 `pandoc-server` is a web server that can perform pandoc conversions. It can be used either as a running server -or as a CGI program. To use `pandoc-server` as a CGI -program, rename it (or symlink it) as `pandoc-server.cgi`. -(Note: if you symlink it, you may need to adjust your -webserver's configuration in order to allow it to follow -symlinks for the CGI script.) +or as a CGI program. + +To use `pandoc-server` as a CGI program, rename it (or symlink +it) as `pandoc-server.cgi`. (Note: if you symlink it, you may +need to adjust your webserver's configuration in order to allow +it to follow symlinks for the CGI script.) All pandoc functions are run in the PandocPure monad, which ensures that they can do no I/O operations on the server. -This should provide a high degree of security. It does, -however, impose certain limitations: +This should provide a high degree of security. This security +does, however, impose certain limitations: - PDFs cannot be produced. @@ -85,17 +86,38 @@ the first one given is the default. : The output format, possibly with extensions, just as it is specified on the pandoc command line. -`wrapText` (`"auto"|"preserve"|"none"`) +`shift-heading-level-by` (integer, default 0) -: Text wrapping option: either `"auto"` (automatic - hard-wrapping to fit within a column width), `"preserve"` - (insert newlines where they are present in the source), - or `"none"` (don't insert any unnecessary newlines at all). +: Increase or decrease the level of all headings. -`columns` (integer, default 72) +`indented-code-classes` (array of strings) -: Column width (affects text wrapping and calculation of - table column widths in plain text formats) +: List of classes to be applied to indented Markdown code blocks. + +`default-image-extension` (string) + +: Extension to be applied to image sources that lack extensions + (e.g. `".jpg"`). + +`metadata` (JSON map) + +: String-valued metadata. + +`tab-stop` (integer, default 4) + +: Tab stop (spaces per tab). + +`track-changes` (`"accept"|"reject"|"all"`) + +: Specifies what to do with insertions, deletions, and + comments produced by the MS Word "Track Changes" feature. Only + affects docx input. + +`abbreviations` (file path) + +: List of strings to be regarded as abbreviations when + parsing Markdown. See `--abbreviations` in `pandoc(1)` for + details. `standalone` (boolean, default false) @@ -108,119 +130,7 @@ the first one given is the default. : String contents of a document template (see Templates in `pandoc(1)` for the format). -`tabStop` (integer, default 4) - -: Tab stop (spaces per tab). - -`indentedCodeClasses` (array of strings) - -: List of classes to be applied to indented Markdown code blocks. - -`abbreviations` (array of strings) - -: List of strings to be regarded as abbreviations when - parsing Markdown. See `--abbreviations` in `pandoc(1)` for - details. - -`defaultImageExtension` (string) - -: Extension to be applied to image sources that lack extensions - (e.g. `".jpg"`). - -`trackChanges` (`"accept"|"reject"|"all"`) - -: Specifies what to do with insertions, deletions, and - comments produced by the MS Word "Track Changes" feature. Only - affects docx input. - -`stripComments` (boolean, default false) - -: Causes HTML comments to be stripped in Markdown or Textile - source, instead of being passed through to the output format. - -`citeproc` (boolean, default false) - -: Causes citations to be processed using citeproc. See - Citations in `pandoc(1)` for details. - -`citeMethod` (`"citeproc"|"natbib"|"biblatex"`) - -: Determines how citations are formatted in LaTeX output. - -`tableOfContents` (boolean, default false) - -: Include a table of contents (in supported formats). - -`tocDepth` (integer, default 3) - -: Depth of sections to include in the table of contents. - -`numberSections` (boolean, default false) - -: Automatically number sections (in supported formats). - -`numberOffset` (array of integers) - -: Offsets to be added to each component of the section number. - For example, `[1]` will cause the first section to be - numbered "2" and the first subsection "2.1"; `[0,1]` will - cause the first section to be numbered "1" and the first - subsection "1.2." - -`identifierPrefix` (string) - -: Prefix to be added to all automatically-generated identifiers. - -`sectionDivs` (boolean, default false) - -: Arrange the document into a hierarchy of nested sections - based on the headings. - -`htmlQTags` (boolean, default false) - -: Use `<q>` elements in HTML instead of literal quotation marks. - -`listings` (boolean, default false) - -: Use the `listings` package to format code in LaTeX output. - -`referenceLinks` (boolean, default false) - -: Create reference links rather than inline links in Markdown output. - -`setextHeaders` (boolean, default false) - -: Use Setext (underlined) headings instead of ATX (`#`-prefixed) - in Markdown output. - -`preferAscii` (boolean, default false) - -: Use entities and escapes when possible to avoid non-ASCII - characters in the output. - -`referenceLocation` (`"document"|"section"|"block"`) - -: Determines whether link references and footnotes are placed - at the end of the document, the end of the section, or the - end of the block (e.g. paragraph), in - certain formats. (See `pandoc(1)` under `--reference-location`.) - - -`topLevelDivision` (`"default"|"part"|"chapter"|"section"`) - -: Determines how top-level headings are interpreted in - LaTeX, ConTeXt, DocBook, and TEI. The `"default"` value - tries to choose the best interpretation based on heuristics. - -`emailObfuscation` (`"none"|"references"|"javascript"`) - -: Determines how email addresses are obfuscated in HTML. - -`htmlMathMethod` (`"plain"|"webtex"|"gladtex"|"mathml"|"mathjax"|"katex"`) - -: Determines how math is represented in HTML. - -`variables` (JSON mapping) +`variables` (JSON map) : Variables to be interpolated in the template. (See Templates in `pandoc(1)`.) @@ -230,17 +140,32 @@ the first one given is the default. : Dots-per-inch to use for conversions between pixels and other measurements (for image sizes). -`incremental` (boolean, default false) +`wrap` (`"auto"|"preserve"|"none"`) -: If true, lists appear incrementally by default in slide shows. +: Text wrapping option: either `"auto"` (automatic + hard-wrapping to fit within a column width), `"preserve"` + (insert newlines where they are present in the source), + or `"none"` (don't insert any unnecessary newlines at all). -`slideLevel` (integer) +`columns` (integer, default 72) -: Heading level that deterimes slide divisions in slide shows. - The default is to pick the highest heading level under which - there is body text. +: Column width (affects text wrapping and calculation of + table column widths in plain text formats) -`highlightStyle` (string, default `"pygments"`) +`table-of-contents` (boolean, default false) + +: Include a table of contents (in supported formats). + +`toc-depth` (integer, default 3) + +: Depth of sections to include in the table of contents. + +`strip-comments` (boolean, default false) + +: Causes HTML comments to be stripped in Markdown or Textile + source, instead of being passed through to the output format. + +`highlight-style` (string, default `"pygments"`) : Specify the style to use for syntax highlighting of code. Standard styles are `"pygments"` (the default), `"kate"`, @@ -250,27 +175,149 @@ the first one given is the default. case, the relevant file contents must also be included in `files`, see below). -`epubMetadata` (string) +`embed-resources` -: Dublin core XML elements to be used for EPUB metadata. +: Embed images, scripts, styles and other resources in an HTML + document using `data` URIs. Note that this will not work + unless the contents of all external resources are included + under `files`. -`epubChapterLevel` (integer, default 1) +`html-q-tags` (boolean, default false) + +: Use `<q>` elements in HTML instead of literal quotation marks. + +`ascii` (boolean, default false) + +: Use entities and escapes when possible to avoid non-ASCII + characters in the output. + +`reference-links` (boolean, default false) + +: Create reference links rather than inline links in Markdown output. + +`referenceLocation` (`"document"|"section"|"block"`) + +: Determines whether link references and footnotes are placed + at the end of the document, the end of the section, or the + end of the block (e.g. paragraph), in + certain formats. (See `pandoc(1)` under `--reference-location`.) + +`setext-headers` (boolean, default false) + +: Use Setext (underlined) headings instead of ATX (`#`-prefixed) + in Markdown output. + +`top-level-division` (`"default"|"part"|"chapter"|"section"`) + +: Determines how top-level headings are interpreted in + LaTeX, ConTeXt, DocBook, and TEI. The `"default"` value + tries to choose the best interpretation based on heuristics. + +`number-sections` (boolean, default false) + +: Automatically number sections (in supported formats). + + +`number-offset` (array of integers) + +: Offsets to be added to each component of the section number. + For example, `[1]` will cause the first section to be + numbered "2" and the first subsection "2.1"; `[0,1]` will + cause the first section to be numbered "1" and the first + subsection "1.2." + +`html-math-method` (`"plain"|"webtex"|"gladtex"|"mathml"|"mathjax"|"katex"`) + +: Determines how math is represented in HTML. + +`listings` (boolean, default false) + +: Use the `listings` package to format code in LaTeX output. + +`incremental` (boolean, default false) + +: If true, lists appear incrementally by default in slide shows. + +`slide-level` (integer) + +: Heading level that deterimes slide divisions in slide shows. + The default is to pick the highest heading level under which + there is body text. + +`section-divs` (boolean, default false) + +: Arrange the document into a hierarchy of nested sections + based on the headings. + +`email-obfuscation` (`"none"|"references"|"javascript"`) + +: Determines how email addresses are obfuscated in HTML. + +`identifier-prefix` (string) + +: Prefix to be added to all automatically-generated identifiers. + +`title-prefix` (string) + +: Prefix to be added to the title in the HTML header. + +`reference-doc` (file path) + +: Reference doc to use in creating `docx` or `odt` or `pptx`. + See `pandoc(1)` under `--reference-doc` for details. + The contents of the file must be included under `files`. + +`epub-cover-image` (file path) + +: Cover image for EPUB. + The contents of the file must be included under `files`. + +`epub-metadata` (file path) + +: Path of file containing Dublin core XML elements to be used for + EPUB metadata. The contents of the file must be included + under `files`. + +`epub-chapter-level` (integer, default 1) : Heading level at which chapter splitting occurs in EPUBs. -`epubSubdirectory` (string, default "EPUB") +`epub-subdirectory` (string, default "EPUB") : Name of content subdirectory in the EPUB container. -`epubFonts` (array of file paths) +`epub-fonts` (array of file paths) : Fonts to include in the EPUB. The fonts themselves must be included in `files` (see below). -`referenceDoc` (file path) +`ipynb-output` (`"best"|"all"|"none"`) -: Reference doc to use in creating `docx` or `odt` or `pptx`. - See `pandoc(1)` under `--reference-doc` for details. +: Determines how ipynb output cells are treated. `all` means + that all of the data formats included in the original are + preserved. `none` means that the contents of data cells + are omitted. `best` causes pandoc to try to pick the + richest data block in each output cell that is compatible + with the output format. + +`citeproc` (boolean, default false) + +: Causes citations to be processed using citeproc. See + Citations in `pandoc(1)` for details. + +`bibliography` (array of file paths) + +: Files containing bibliographic data. The contents of the + files must be included in `files`. + +`csl` (file path) + +: CSL style file. The contents of the file must be included + in `files`. + +`cite-method` (`"citeproc"|"natbib"|"biblatex"`) + +: Determines how citations are formatted in LaTeX output. `files` (JSON mapping of file paths to base64-encoded strings) @@ -280,7 +327,6 @@ the first one given is the default. left as it is, unless it is *also* valid base 64 data, in which case it will be interpreted that way. - ## `/batch` endpoint The `/batch` endpoint behaves like the root endpoint, diff --git a/linux/pandoc.control.in b/linux/control.in similarity index 100% rename from linux/pandoc.control.in rename to linux/control.in diff --git a/linux/make_artifacts.sh b/linux/make_artifacts.sh index 5e594b569..4653a0c19 100644 --- a/linux/make_artifacts.sh +++ b/linux/make_artifacts.sh @@ -27,42 +27,44 @@ ghc --version cabal update cabal clean -cabal configure -fserver -f-export-dynamic -fembed_data_files --enable-executable-static --ghc-options '-j4 +RTS -A256m -RTS -split-sections -optc-Os -optl=-pthread' pandoc pandoc-server +cabal configure -f-export-dynamic -fembed_data_files --enable-executable-static --ghc-options '-j4 +RTS -A256m -RTS -split-sections -optc-Os -optl=-pthread' pandoc cabal build -j4 for f in $(find dist-newstyle -name 'pandoc' -type f -perm /400); do cp $f $ARTIFACTS/; done -for f in $(find dist-newstyle -name 'pandoc-server' -type f -perm /400); do cp $f /$ARTIFACTS/; done # Confirm that we have static builds file $ARTIFACTS/pandoc | grep "statically linked" -file $ARTIFACTS/pandoc-server | grep "statically linked" -# make deb for EXE make_deb() { - VERSION=`$ARTIFACTS/$EXE --version | awk '{print $2; exit;}'` + VERSION=`$ARTIFACTS/pandoc --version | awk '{print $2; exit;}'` REVISION=${REVISION:-1} DEBVER=$VERSION-$REVISION - BASE=$EXE-$DEBVER-$ARCHITECTURE + BASE=pandoc-$DEBVER-$ARCHITECTURE DIST=/mnt/$BASE DEST=$DIST/usr - COPYRIGHT=$DEST/share/doc/$EXE/copyright + COPYRIGHT=$DEST/share/doc/pandoc/copyright cd /mnt mkdir -p $DEST/bin mkdir -p $DEST/share/man/man1 - mkdir -p $DEST/share/doc/$EXE + mkdir -p $DEST/share/doc/pandoc find $DIST -type d | xargs chmod 755 - cp $ARTIFACTS/$EXE $DEST/bin/ - strip $DEST/bin/$EXE - cp /mnt/man/$EXE.1 $DEST/share/man/man1/$EXE.1 - gzip -9 $DEST/share/man/man1/$EXE.1 + cp $ARTIFACTS/pandoc $DEST/bin/ + cd $DEST/bin + strip pandoc + ln -s pandoc pandoc-server + cd /mnt + cp /mnt/man/pandoc.1 $DEST/share/man/man1/pandoc.1 + gzip -9 $DEST/share/man/man1/pandoc.1 + cp /mnt/man/pandoc-server.1 $DEST/share/man/man1/pandoc-server.1 + gzip -9 $DEST/share/man/man1/pandoc-server.1 cp /mnt/COPYRIGHT $COPYRIGHT echo "" >> $COPYRIGHT INSTALLED_SIZE=$(du -k -s $DEST | awk '{print $1}') mkdir $DIST/DEBIAN - perl -pe "s/VERSION/$DEBVER/" /mnt/linux/$EXE.control.in | \ + perl -pe "s/VERSION/$DEBVER/" /mnt/linux/control.in | \ perl -pe "s/ARCHITECTURE/$ARCHITECTURE/" | \ perl -pe "s/INSTALLED_SIZE/$INSTALLED_SIZE/" \ > $DIST/DEBIAN/control @@ -73,26 +75,28 @@ make_deb() { cp $BASE.deb $ARTIFACTS/ } -# Make tarball for EXE +# Make tarball for pandoc make_tarball() { - TARGET=$EXE-$VERSION + TARGET=pandoc-$VERSION cd $ARTIFACTS rm -rf $TARGET mkdir $TARGET mkdir $TARGET/bin $TARGET/share $TARGET/share/man $TARGET/share/man/man1 - cp /mnt/man/$EXE.1 $TARGET/share/man/man1 - mv $EXE $TARGET/bin - strip $TARGET/bin/$EXE - gzip -9 $TARGET/share/man/man1/$EXE.1 + cp /mnt/man/pandoc.1 $TARGET/share/man/man1 + cp /mnt/man/pandoc-server.1 $TARGET/share/man/man1 + mv pandoc $TARGET/bin + cd $TARGET/bin + strip pandoc + ln -s pandoc pandoc-server + cd $ARTIFACTS + gzip -9 $TARGET/share/man/man1/pandoc.1 + gzip -9 $TARGET/share/man/man1/pandoc-server.1 tar cvzf $TARGET-linux-$ARCHITECTURE.tar.gz $TARGET rm -r $TARGET } -for EXE in pandoc pandoc-server -do - make_deb - make_tarball -done +make_deb +make_tarball exit 0 diff --git a/linux/pandoc-server.control.in b/linux/pandoc-server.control.in deleted file mode 100644 index 348fd21c4..000000000 --- a/linux/pandoc-server.control.in +++ /dev/null @@ -1,9 +0,0 @@ -Package: pandoc-server -Version: VERSION -Section: text -Priority: optional -Architecture: ARCHITECTURE -Installed-Size: INSTALLED_SIZE -Depends: libc6 (>= 2.13), libgmp10, zlib1g (>= 1:1.1.4) -Maintainer: John MacFarlane <jgm@berkeley.edu> -Description: HTTP server for pandoc document format converter diff --git a/pandoc.cabal b/pandoc.cabal index 275e87f90..592956c55 100644 --- a/pandoc.cabal +++ b/pandoc.cabal @@ -429,10 +429,6 @@ flag lua53 Description: Embed Lua 5.3 instead of 5.4. Default: False -flag server - Description: Build pandoc-server executable. - Default: False - flag nightly Description: Add '-nightly-COMPILEDATE' to the output of '--version'. Default: False @@ -538,7 +534,10 @@ library xml-types >= 0.3 && < 0.4, yaml >= 0.11 && < 0.12, zip-archive >= 0.2.3.4 && < 0.5, - zlib >= 0.5 && < 0.7 + zlib >= 0.5 && < 0.7, + servant-server, + wai >= 0.3 + if !os(windows) build-depends: unix >= 2.4 && < 2.8 if flag(nightly) @@ -564,6 +563,7 @@ library Text.Pandoc.MediaBag, Text.Pandoc.Error, Text.Pandoc.Filter, + Text.Pandoc.Server, Text.Pandoc.Readers, Text.Pandoc.Readers.HTML, Text.Pandoc.Readers.LaTeX, @@ -789,32 +789,8 @@ executable pandoc main-is: pandoc.hs buildable: True other-modules: Paths_pandoc - -executable pandoc-server - import: common-executable - main-is: Main.hs - other-modules: PandocServer - hs-source-dirs: server - if flag(server) - build-depends: base, - pandoc, - aeson, - text, - containers, - data-default, - bytestring, - skylighting, - base64 >= 0.4, - doctemplates, - servant-server, - wai >= 0.3, - wai-extra >= 3.0.24, - warp, - optparse-applicative - - buildable: True - else - buildable: False + build-depends: wai-extra >= 3.0.24, + warp test-suite test-pandoc import: common-executable diff --git a/server/Main.hs b/server/Main.hs deleted file mode 100644 index 531a0b0a0..000000000 --- a/server/Main.hs +++ /dev/null @@ -1,54 +0,0 @@ -module Main where - -import PandocServer (app) -import Text.Pandoc (pandocVersion) -import Control.Monad (when) -import qualified Network.Wai.Handler.CGI as CGI -import qualified Network.Wai.Handler.Warp as Warp -import Network.Wai.Middleware.Timeout (timeout) -import System.Environment (getProgName) -import Options.Applicative -import System.Exit (exitWith, ExitCode(ExitSuccess)) -import Data.Text as T - -data Opts = Opts - { optPort :: Warp.Port, - optTimeout :: Int, -- seconds - optVersion :: Bool } - -options :: Parser Opts -options = Opts - <$> option auto - ( long "port" - <> value 3030 - <> metavar "PORT" - <> help "Port to serve on" ) - <*> option auto - ( long "timeout" - <> value 2 - <> metavar "SECONDS" - <> help "Seconds timeout" ) - <*> flag False True - ( long "version" - <> help "Print version" ) - -main :: IO () -main = do - progname <- getProgName - let optspec = info (options <**> helper) - ( fullDesc - <> progDesc "Run a pandoc server" - <> header "pandoc-server - text conversion server" ) - opts <- execParser optspec - - when (optVersion opts) $ do - putStrLn $ progname <> " " <> T.unpack pandocVersion - exitWith ExitSuccess - - let port = optPort opts - let app' = timeout (optTimeout opts) app - if progname == "pandoc-server.cgi" - then -- operate as a CGI script - CGI.run app' - else -- operate as a persistent server - Warp.run port app' diff --git a/server/PandocServer.hs b/server/PandocServer.hs deleted file mode 100644 index 295412c6d..000000000 --- a/server/PandocServer.hs +++ /dev/null @@ -1,301 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeOperators #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE OverloadedStrings #-} -module PandocServer - ( app - , Params(..) - ) where - -import Data.Aeson -import Data.Aeson.TH -import Network.Wai -import Servant -import Text.DocTemplates as DocTemplates -import Text.Pandoc -import Text.Pandoc.Citeproc (processCitations) -import Text.Pandoc.Highlighting (lookupHighlightingStyle) -import qualified Text.Pandoc.UTF8 as UTF8 -import Data.Text (Text) -import qualified Data.Text as T -import qualified Data.Text.Lazy as TL -import qualified Data.Text.Lazy.Encoding as TLE -import Data.Maybe (fromMaybe) -import Data.Char (isAlphaNum) -import qualified Data.ByteString as BS -import qualified Data.ByteString.Lazy as BL -import Data.ByteString.Base64 (decodeBase64, encodeBase64) -import Data.Default -import Data.Map (Map) -import Data.Set (Set) -import Skylighting (defaultSyntaxMap) - -newtype Blob = Blob BL.ByteString - deriving (Show, Eq) - -instance ToJSON Blob where - toJSON (Blob bs) = toJSON (encodeBase64 $ BL.toStrict bs) - -instance FromJSON Blob where - parseJSON = withText "Blob" $ \t -> do - let inp = UTF8.fromText t - case decodeBase64 inp of - Right bs -> return $ Blob $ BL.fromStrict bs - Left _ -> -- treat as regular text - return $ Blob $ BL.fromStrict inp - --- This is the data to be supplied by the JSON payload --- of requests. Maybe values may be omitted and will be --- given default values. -data Params = Params - { text :: Text - , from :: Maybe Text - , to :: Maybe Text - , wrapText :: Maybe WrapOption - , columns :: Maybe Int - , standalone :: Maybe Bool - , template :: Maybe Text - , tabStop :: Maybe Int - , indentedCodeClasses :: Maybe [Text] - , abbreviations :: Maybe (Set Text) - , defaultImageExtension :: Maybe Text - , trackChanges :: Maybe TrackChanges - , stripComments :: Maybe Bool - , citeproc :: Maybe Bool - , variables :: Maybe (DocTemplates.Context Text) - , tableOfContents :: Maybe Bool - , incremental :: Maybe Bool - , htmlMathMethod :: Maybe HTMLMathMethod - , numberSections :: Maybe Bool - , numberOffset :: Maybe [Int] - , sectionDivs :: Maybe Bool - , referenceLinks :: Maybe Bool - , dpi :: Maybe Int - , emailObfuscation :: Maybe ObfuscationMethod - , identifierPrefix :: Maybe Text - , citeMethod :: Maybe CiteMethod - , htmlQTags :: Maybe Bool - , slideLevel :: Maybe Int - , topLevelDivision :: Maybe TopLevelDivision - , listings :: Maybe Bool - , highlightStyle :: Maybe Text - , setextHeaders :: Maybe Bool - , epubSubdirectory :: Maybe Text - , epubFonts :: Maybe [FilePath] - , epubMetadata :: Maybe Text - , epubChapterLevel :: Maybe Int - , tocDepth :: Maybe Int - , referenceDoc :: Maybe FilePath - , referenceLocation :: Maybe ReferenceLocation - , preferAscii :: Maybe Bool - , files :: Maybe (Map FilePath Blob) - } deriving (Show) - -instance Default Params where - def = Params - { text = "" - , from = Nothing - , to = Nothing - , wrapText = Nothing - , columns = Nothing - , standalone = Nothing - , template = Nothing - , tabStop = Nothing - , indentedCodeClasses = Nothing - , abbreviations = Nothing - , defaultImageExtension = Nothing - , trackChanges = Nothing - , stripComments = Nothing - , citeproc = Nothing - , variables = Nothing - , tableOfContents = Nothing - , incremental = Nothing - , htmlMathMethod = Nothing - , numberSections = Nothing - , numberOffset = Nothing - , sectionDivs = Nothing - , referenceLinks = Nothing - , dpi = Nothing - , emailObfuscation = Nothing - , identifierPrefix = Nothing - , citeMethod = Nothing - , htmlQTags = Nothing - , slideLevel = Nothing - , topLevelDivision = Nothing - , listings = Nothing - , highlightStyle = Nothing - , setextHeaders = Nothing - , epubSubdirectory = Nothing - , epubMetadata = Nothing - , epubChapterLevel = Nothing - , epubFonts = Nothing - , tocDepth = Nothing - , referenceDoc = Nothing - , referenceLocation = Nothing - , preferAscii = Nothing - , files = Nothing - } - -- TODO: - -- shiftHeadingLevelBy - -- metadata - -- selfContained - -- embedResources - -- epubCoverImage - -- stripEmptyParagraphs - -- titlePrefix - -- ipynbOutput - -- eol - -- csl - -- bibliography - -- citationAbbreviations - --- Automatically derive code to convert to/from JSON. -$(deriveJSON defaultOptions ''Params) - --- This is the API. The "/convert" endpoint takes a request body --- consisting of a JSON-encoded Params structure and responds to --- Get requests with either plain text or JSON, depending on the --- Accept header. -type API = - ReqBody '[JSON] Params :> Post '[PlainText, JSON] Text - :<|> - ReqBody '[JSON] Params :> Post '[OctetStream] BS.ByteString - :<|> - "batch" :> ReqBody '[JSON] [Params] :> Post '[JSON] [Text] - :<|> - "babelmark" :> QueryParam' '[Required] "text" Text :> QueryParam "from" Text :> QueryParam "to" Text :> QueryFlag "standalone" :> Get '[JSON] Value - :<|> - "version" :> Get '[PlainText, JSON] Text - -app :: Application -app = serve api server - -api :: Proxy API -api = Proxy - -server :: Server API -server = convert - :<|> convertBytes - :<|> mapM convert - :<|> babelmark -- for babelmark which expects {"html": "", "version": ""} - :<|> pure pandocVersion - where - babelmark text' from' to' standalone' = do - res <- convert def{ text = text', - from = from', to = to', - standalone = Just standalone' } - return $ toJSON $ object [ "html" .= res, "version" .= pandocVersion ] - - -- We use runPure for the pandoc conversions, which ensures that - -- they will do no IO. This makes the server safe to use. However, - -- it will mean that features requiring IO, like RST includes, will not work. - -- Changing this to - -- handleErr =<< liftIO (runIO (convert' params)) - -- will allow the IO operations. - convert params = handleErr $ - runPure (convert' id (encodeBase64 . BL.toStrict) params) - - convertBytes params = handleErr $ - runPure (convert' UTF8.fromText BL.toStrict params) - - convert' :: PandocMonad m - => (Text -> a) -> (BL.ByteString -> a) -> Params -> m a - convert' textHandler bsHandler params = do - let readerFormat = fromMaybe "markdown" $ from params - let writerFormat = fromMaybe "html" $ to params - (readerSpec, readerExts) <- getReader readerFormat - (writerSpec, writerExts) <- getWriter writerFormat - let binaryOutput = case writerSpec of - ByteStringWriter{} -> True - _ -> False - let isStandalone = fromMaybe binaryOutput (standalone params) - let toformat = T.toLower $ T.takeWhile isAlphaNum $ writerFormat - hlStyle <- traverse (lookupHighlightingStyle . T.unpack) - $ highlightStyle params - mbTemplate <- if isStandalone - then case template params of - Nothing -> Just <$> - compileDefaultTemplate toformat - Just t -> Just <$> - compileCustomTemplate toformat t - else return Nothing - let readeropts = def{ readerExtensions = readerExts - , readerStandalone = isStandalone - , readerTabStop = fromMaybe 4 (tabStop params) - , readerIndentedCodeClasses = fromMaybe [] - (indentedCodeClasses params) - , readerAbbreviations = - fromMaybe mempty (abbreviations params) - , readerDefaultImageExtension = - fromMaybe mempty (defaultImageExtension params) - , readerTrackChanges = - fromMaybe AcceptChanges (trackChanges params) - , readerStripComments = - fromMaybe False (stripComments params) - } - let writeropts = - def{ writerExtensions = writerExts - , writerTabStop = fromMaybe 4 (tabStop params) - , writerWrapText = fromMaybe WrapAuto (wrapText params) - , writerColumns = fromMaybe 72 (columns params) - , writerTemplate = mbTemplate - , writerSyntaxMap = defaultSyntaxMap - , writerVariables = fromMaybe mempty (variables params) - , writerTableOfContents = fromMaybe False (tableOfContents params) - , writerIncremental = fromMaybe False (incremental params) - , writerHTMLMathMethod = - fromMaybe PlainMath (htmlMathMethod params) - , writerNumberSections = fromMaybe False (numberSections params) - , writerNumberOffset = fromMaybe [] (numberOffset params) - , writerSectionDivs = fromMaybe False (sectionDivs params) - , writerReferenceLinks = fromMaybe False (referenceLinks params) - , writerDpi = fromMaybe 96 (dpi params) - , writerEmailObfuscation = - fromMaybe NoObfuscation (emailObfuscation params) - , writerIdentifierPrefix = - fromMaybe mempty (identifierPrefix params) - , writerCiteMethod = fromMaybe Citeproc (citeMethod params) - , writerHtmlQTags = fromMaybe False (htmlQTags params) - , writerSlideLevel = slideLevel params - , writerTopLevelDivision = - fromMaybe TopLevelDefault (topLevelDivision params) - , writerListings = fromMaybe False (listings params) - , writerHighlightStyle = hlStyle - , writerSetextHeaders = fromMaybe False (setextHeaders params) - , writerEpubSubdirectory = - fromMaybe "EPUB" (epubSubdirectory params) - , writerEpubMetadata = epubMetadata params - , writerEpubFonts = fromMaybe [] (epubFonts params) - , writerEpubChapterLevel = fromMaybe 1 (epubChapterLevel params) - , writerTOCDepth = fromMaybe 3 (tocDepth params) - , writerReferenceDoc = referenceDoc params - , writerReferenceLocation = - fromMaybe EndOfDocument (referenceLocation params) - , writerPreferAscii = fromMaybe False (preferAscii params) - } - let reader = case readerSpec of - TextReader r -> r readeropts - ByteStringReader r -> \t -> do - let eitherbs = decodeBase64 $ UTF8.fromText t - case eitherbs of - Left errt -> throwError $ PandocSomeError errt - Right bs -> r readeropts $ BL.fromStrict bs - let writer = case writerSpec of - TextWriter w -> fmap textHandler . w writeropts - ByteStringWriter w -> fmap bsHandler . w writeropts - reader (text params) >>= - (if citeproc params == Just True - then processCitations - else return) >>= - writer - - handleErr (Right t) = return t - handleErr (Left err) = throwError $ - err500 { errBody = TLE.encodeUtf8 $ TL.fromStrict $ renderError err } - - compileCustomTemplate toformat t = do - res <- runWithPartials $ compileTemplate ("custom." <> T.unpack toformat) t - case res of - Left e -> throwError $ PandocTemplateError (T.pack e) - Right tpl -> return tpl diff --git a/src/Text/Pandoc/Server.hs b/src/Text/Pandoc/Server.hs new file mode 100644 index 000000000..a7c46f93f --- /dev/null +++ b/src/Text/Pandoc/Server.hs @@ -0,0 +1,357 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE OverloadedStrings #-} +module Text.Pandoc.Server + ( app + , ServerOpts(..) + , Params(..) + , Blob(..) + , parseServerOpts + ) where + +import Data.Aeson +import Network.Wai +import Servant +import Text.DocTemplates as DocTemplates +import Text.Pandoc +import Text.Pandoc.Citeproc (processCitations) +import Text.Pandoc.Highlighting (lookupHighlightingStyle) +import qualified Text.Pandoc.UTF8 as UTF8 +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Text.Lazy as TL +import qualified Data.Text.Lazy.Encoding as TLE +import Data.Maybe (fromMaybe) +import Data.Char (isAlphaNum) +import qualified Data.ByteString as BS +import qualified Data.ByteString.Lazy as BL +import Data.ByteString.Base64 (decodeBase64, encodeBase64) +import Data.Default +import Control.Monad (when, foldM) +import qualified Data.Set as Set +import Skylighting (defaultSyntaxMap) +import qualified Data.Map as M +import System.Console.GetOpt +import System.Environment (getArgs, getProgName) +import qualified Control.Exception as E +import Text.Pandoc.Shared (safeStrRead, headerShift, filterIpynbOutput, + eastAsianLineBreakFilter, stripEmptyParagraphs) +import Text.Pandoc.App.Opt ( IpynbOutput (..), Opt(..), defaultOpts ) +import Text.Pandoc.Filter (Filter(..)) +import Text.Pandoc.Builder (setMeta) +import Text.Pandoc.SelfContained (makeSelfContained) +import System.Exit + +data ServerOpts = + ServerOpts + { serverPort :: Int + , serverTimeout :: Int } + deriving (Show) + +defaultServerOpts :: ServerOpts +defaultServerOpts = ServerOpts { serverPort = 3030, serverTimeout = 2 } + +cliOptions :: [OptDescr (ServerOpts -> IO ServerOpts)] +cliOptions = + [ Option ['p'] ["port"] + (ReqArg (\s opts -> case safeStrRead s of + Just i -> return opts{ serverPort = i } + Nothing -> + E.throwIO $ PandocOptionError $ T.pack + s <> " is not a number") "NUMBER") + "port number" + , Option ['t'] ["timeout"] + (ReqArg (\s opts -> case safeStrRead s of + Just i -> return opts{ serverTimeout = i } + Nothing -> + E.throwIO $ PandocOptionError $ T.pack + s <> " is not a number") "NUMBER") + "timeout (seconds)" + + , Option ['h'] ["help"] + (NoArg (\_ -> do + prg <- getProgName + let header = "Usage: " <> prg <> " [OPTION...]" + putStrLn $ usageInfo header cliOptions + exitWith ExitSuccess)) + "help message" + + , Option ['v'] ["version"] + (NoArg (\_ -> do + prg <- getProgName + putStrLn $ prg <> " " <> T.unpack pandocVersion + exitWith ExitSuccess)) + "version info" + + ] + +parseServerOpts :: IO ServerOpts +parseServerOpts = do + args <- getArgs + let handleUnknownOpt x = "Unknown option: " <> x + case getOpt' Permute cliOptions args of + (os, ns, unrecognizedOpts, es) -> do + when (not (null es) || not (null unrecognizedOpts)) $ + E.throwIO $ PandocOptionError $ T.pack $ + concat es ++ unlines (map handleUnknownOpt unrecognizedOpts) ++ + ("Try --help for more information.") + when (not (null ns)) $ + E.throwIO $ PandocOptionError $ T.pack $ + "Unknown arguments: " <> unwords ns + foldM (flip ($)) defaultServerOpts os + +newtype Blob = Blob BL.ByteString + deriving (Show, Eq) + +instance ToJSON Blob where + toJSON (Blob bs) = toJSON (encodeBase64 $ BL.toStrict bs) + +instance FromJSON Blob where + parseJSON = withText "Blob" $ \t -> do + let inp = UTF8.fromText t + case decodeBase64 inp of + Right bs -> return $ Blob $ BL.fromStrict bs + Left _ -> -- treat as regular text + return $ Blob $ BL.fromStrict inp + +-- This is the data to be supplied by the JSON payload +-- of requests. Maybe values may be omitted and will be +-- given default values. +data Params = Params + { options :: Opt + , text :: Text + , files :: Maybe (M.Map FilePath Blob) + } deriving (Show) + +instance Default Params where + def = Params + { options = defaultOpts + , text = mempty + , files = Nothing + } + +-- Automatically derive code to convert to/from JSON. +instance FromJSON Params where + parseJSON = withObject "Params" $ \o -> + Params + <$> parseJSON (Object o) + <*> o .: "text" + <*> o .:? "files" + + +-- This is the API. The "/convert" endpoint takes a request body +-- consisting of a JSON-encoded Params structure and responds to +-- Get requests with either plain text or JSON, depending on the +-- Accept header. +type API = + ReqBody '[JSON] Params :> Post '[PlainText, JSON] Text + :<|> + ReqBody '[JSON] Params :> Post '[OctetStream] BS.ByteString + :<|> + "batch" :> ReqBody '[JSON] [Params] :> Post '[JSON] [Text] + :<|> + "babelmark" :> QueryParam' '[Required] "text" Text :> QueryParam "from" Text :> QueryParam "to" Text :> QueryFlag "standalone" :> Get '[JSON] Value + :<|> + "version" :> Get '[PlainText, JSON] Text + +app :: Application +app = serve api server + +api :: Proxy API +api = Proxy + +server :: Server API +server = convert + :<|> convertBytes + :<|> mapM convert + :<|> babelmark -- for babelmark which expects {"html": "", "version": ""} + :<|> pure pandocVersion + where + babelmark text' from' to' standalone' = do + res <- convert def{ text = text', + options = defaultOpts{ + optFrom = from', + optTo = to', + optStandalone = standalone' } + } + return $ toJSON $ object [ "html" .= res, "version" .= pandocVersion ] + + -- We use runPure for the pandoc conversions, which ensures that + -- they will do no IO. This makes the server safe to use. However, + -- it will mean that features requiring IO, like RST includes, will not work. + -- Changing this to + -- handleErr =<< liftIO (runIO (convert' params)) + -- will allow the IO operations. + convert params = handleErr $ + runPure (convert' id (encodeBase64 . BL.toStrict) params) + + convertBytes params = handleErr $ + runPure (convert' UTF8.fromText BL.toStrict params) + + convert' :: (Text -> a) -> (BL.ByteString -> a) -> Params -> PandocPure a + convert' textHandler bsHandler params = do + curtime <- getCurrentTime + -- put files params in ersatz file system + let addFile :: FilePath -> Blob -> FileTree -> FileTree + addFile fp (Blob lbs) = + insertInFileTree fp FileInfo{ infoFileMTime = curtime + , infoFileContents = BL.toStrict lbs } + case files params of + Nothing -> return () + Just fs -> do + let filetree = M.foldrWithKey addFile mempty fs + modifyPureState $ \st -> st{ stFiles = filetree } + + let opts = options params + let readerFormat = fromMaybe "markdown" $ optFrom opts + let writerFormat = fromMaybe "html" $ optTo opts + (readerSpec, readerExts) <- getReader readerFormat + (writerSpec, writerExts) <- getWriter writerFormat + + let isStandalone = optStandalone opts + let toformat = T.toLower $ T.takeWhile isAlphaNum $ writerFormat + hlStyle <- traverse (lookupHighlightingStyle . T.unpack) + $ optHighlightStyle opts + + mbTemplate <- if isStandalone + then case optTemplate opts of + Nothing -> Just <$> + compileDefaultTemplate toformat + Just t -> Just <$> + compileCustomTemplate toformat t + else return Nothing + + abbrevs <- Set.fromList . filter (not . T.null) . T.lines . UTF8.toText <$> + case optAbbreviations opts of + Nothing -> readDataFile "abbreviations" + Just f -> readFileStrict f + + let readeropts = def{ readerExtensions = readerExts + , readerStandalone = isStandalone + , readerTabStop = optTabStop opts + , readerIndentedCodeClasses = + optIndentedCodeClasses opts + , readerAbbreviations = abbrevs + , readerDefaultImageExtension = + optDefaultImageExtension opts + , readerTrackChanges = optTrackChanges opts + , readerStripComments = optStripComments opts + } + let writeropts = + def{ writerExtensions = writerExts + , writerTabStop = optTabStop opts + , writerWrapText = optWrap opts + , writerColumns = optColumns opts + , writerTemplate = mbTemplate + , writerSyntaxMap = defaultSyntaxMap + , writerVariables = optVariables opts + , writerTableOfContents = optTableOfContents opts + , writerIncremental = optIncremental opts + , writerHTMLMathMethod = optHTMLMathMethod opts + , writerNumberSections = optNumberSections opts + , writerNumberOffset = optNumberOffset opts + , writerSectionDivs = optSectionDivs opts + , writerReferenceLinks = optReferenceLinks opts + , writerDpi = optDpi opts + , writerEmailObfuscation = optEmailObfuscation opts + , writerIdentifierPrefix = optIdentifierPrefix opts + , writerCiteMethod = optCiteMethod opts + , writerHtmlQTags = optHtmlQTags opts + , writerSlideLevel = optSlideLevel opts + , writerTopLevelDivision = optTopLevelDivision opts + , writerListings = optListings opts + , writerHighlightStyle = hlStyle + , writerSetextHeaders = optSetextHeaders opts + , writerEpubSubdirectory = T.pack $ optEpubSubdirectory opts + , writerEpubMetadata = T.pack <$> optEpubMetadata opts + , writerEpubFonts = optEpubFonts opts + , writerEpubChapterLevel = optEpubChapterLevel opts + , writerTOCDepth = optTOCDepth opts + , writerReferenceDoc = optReferenceDoc opts + , writerReferenceLocation = optReferenceLocation opts + , writerPreferAscii = optAscii opts + } + let reader = case readerSpec of + TextReader r -> r readeropts + ByteStringReader r -> \t -> do + let eitherbs = decodeBase64 $ UTF8.fromText t + case eitherbs of + Left errt -> throwError $ PandocSomeError errt + Right bs -> r readeropts $ BL.fromStrict bs + let writer = case writerSpec of + TextWriter w -> + fmap textHandler . + (\d -> w writeropts d >>= + if optEmbedResources opts && htmlFormat (optTo opts) + then makeSelfContained + else return) + ByteStringWriter w -> fmap bsHandler . w writeropts + + let transforms :: Pandoc -> Pandoc + transforms = (case optShiftHeadingLevelBy opts of + 0 -> id + x -> headerShift x) . + (case optStripEmptyParagraphs opts of + True -> stripEmptyParagraphs + False -> id) . + (if extensionEnabled Ext_east_asian_line_breaks + readerExts && + not (extensionEnabled Ext_east_asian_line_breaks + writerExts && + optWrap opts == WrapPreserve) + then eastAsianLineBreakFilter + else id) . + (case optIpynbOutput opts of + IpynbOutputAll -> id + IpynbOutputNone -> filterIpynbOutput Nothing + IpynbOutputBest -> filterIpynbOutput (Just $ + case optTo opts of + Just "latex" -> Format "latex" + Just "beamer" -> Format "latex" + Nothing -> Format "html" + Just f + | htmlFormat (optTo opts) -> Format "html" + | otherwise -> Format f)) + + let meta = (case optBibliography opts of + [] -> id + fs -> setMeta "bibliography" (MetaList + (map (MetaString . T.pack) fs))) . + maybe id (setMeta "csl" . MetaString . T.pack) + (optCSL opts) . + maybe id (setMeta "citation-abbreviations" . MetaString . + T.pack) + (optCitationAbbreviations opts) $ + optMetadata opts + + let addMetadata m' (Pandoc m bs) = Pandoc (m <> m') bs + + let hasCiteprocFilter [] = False + hasCiteprocFilter (CiteprocFilter:_) = True + hasCiteprocFilter (_:xs) = hasCiteprocFilter xs + + reader (text params) >>= + return . transforms . addMetadata meta >>= + (if hasCiteprocFilter (optFilters opts) + then processCitations + else return) >>= + writer + + htmlFormat :: Maybe Text -> Bool + htmlFormat Nothing = True + htmlFormat (Just f) = + any (`T.isPrefixOf` f) + ["html","html4","html5","s5","slidy", "slideous","dzslides","revealjs"] + + handleErr (Right t) = return t + handleErr (Left err) = throwError $ + err500 { errBody = TLE.encodeUtf8 $ TL.fromStrict $ renderError err } + + compileCustomTemplate toformat t = do + res <- runWithPartials $ compileTemplate ("custom." <> T.unpack toformat) + (T.pack t) + case res of + Left e -> throwError $ PandocTemplateError (T.pack e) + Right tpl -> return tpl + diff --git a/stack.yaml b/stack.yaml index b0558fbb9..d0c551a8d 100644 --- a/stack.yaml +++ b/stack.yaml @@ -1,6 +1,5 @@ flags: pandoc: - server: false embed_data_files: true QuickCheck: old-random: false