packages feed

pdf-slave (empty) → 1.0.0.0

raw patch · 10 files changed

+1169/−0 lines, 10 filesdep +HaTeXdep +aesondep +basesetup-changed

Dependencies added: HaTeX, aeson, base, base64-bytestring, bytestring, containers, exceptions, haskintex, optparse-applicative, pdf-slave, shelly, system-filepath, text, unordered-containers, yaml

Files

+ CHANGELOG.md view
@@ -0,0 +1,4 @@+0.1.0.0+=======++* Initial release.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Author name here (c) 2016++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Author name here nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,286 @@+pdf-slave+=========++Tool that compiles `haskintex` (TeX with embedded Haskell) files into PDF documents.+Templates are described in YAML format and can define dependencies.++Features:++* Input JSON file for `htex` file that holds template varying data.++* Option for packing a template in all-in YAML bundle.++* Support for template dependencies that include:+  - Bibtex files+  - Images, listings, other static files.+  - Other `.htex` templates that compiles into TeX and includes into parent template.+  - Other `.htex` templates that compiles into PDF and includes into parent template.++Template reference+==================++Common template consists of several files:++* `template_input.json` - Input data for template in JSON format.++``` JSON+{+  "line-width": 2,+  "spiral-precision": 0.01,+  "spiral-interval": [0,4],+  "spiral-a": 0.1,+  "spiral-b": 4+}+```++* `template.htex` - TeX/LaTeX with embedded Haskell that reads input data from+file `template.json`. You have to provide code at the beginning of file that reads+the inputs, like that:++``` Haskell+\begin{document}++\begin{haskellpragmas}+{-# LANGUAGE OverloadedStrings #-}+\end{haskellpragmas}+\begin{writehaskell}+import Data.Aeson+import System.IO.Unsafe (unsafePerformIO)+import qualified Data.ByteString.Lazy as BS++data Input = Input {+  lineWidth       :: Double+, spiralPrecision :: Double+, spiralInterval  :: (Double, Double)+, spiralA         :: Double+, spiralB         :: Double+}++instance FromJSON Input where+  parseJSON (Object o) = Input+    <$> o .: "line-width"+    <*> o .: "spiral-precision"+    <*> o .: "spiral-interval"+    <*> o .: "spiral-a"+    <*> o .: "spiral-b"++inpt :: Input+inpt = case unsafePerformIO $ fmap eitherDecode' $ BS.readFile "template_input.json" of+  Left e -> error (show e)+  Right a -> a++\end{writehaskell}+```++* `template.yaml` - description of template and its dependencies.++``` YAML+name:  template01             # name of template+input: template01_input.json  # name of input file+body:  template01.htex        # name of .htex file+dependencies: {}              # dependency tree (see below)+haskintex-opts: []            # additional flags to haskintex+```++## Dependencies++There are 4 different types of dependencies:++* `other` - static files that don't require any processing. They could be images,+listings etc. Example of YAML configuration:++  ``` YAML+  dependencies:+    lambda.png: # name of dependency is equal to filename relative to parent template+      type: other # marks type of dependency+    code/demo.hs:+      type: other+  ```++  See [examples/example02](https://github.com/NCrashed/pdf-slave/tree/master/examples/template02) for full example.++* `bibtex` - bibliography files that require additional passes with `bibtex`. Example of YAML configuration:++  ``` YAML+  dependencies:+    biblio.bib: # name of dependency is equal to filename relative to parent template+      type: bibtex # marks type of dependency+  ```++  See [examples/example03](https://github.com/NCrashed/pdf-slave/tree/master/examples/template03) for full example.++* `template` - other `.htex` templates that are included in parent via `\\input{...}` or `\\include{..}`. Example of YAML configuration:++  ``` YAML+  dependencies:+    dep1: # name of dependency defines subfolder where the output tex file is located+      type:  template # marks type of dependency+      name:  dep1+      input: dep1_input.json+      body:  dep1.htex+    dep2:+      type:  template+      name:  dep2+      body:  dep2.htex+      dependencies:+        lambda.png:+          type: other+        code/demo.hs:+          type: other+  ```++  See [examples/example04](https://github.com/NCrashed/pdf-slave/tree/master/examples/template04) for full example.++  **Note that `code/demo.hs` subdependency should be included as `dep2/code/demo.hs` in `dep2.htex` as `dep2.tex` is inlined into parent.**++* `template_pdf` - other `.htex` templates that are included as PDFs into parent template. Example of YAML configuration:++  ``` YAML+  dependencies:+    template01: # name of dependency defines subfolder where the output tex file is located+      type:  template_pdf # marks type of dependency+      name:  template01+      input: template01_input.json+      body:  template01.htex+    template02:+      type:  template_pdf+      name:  template02+      body:  template02.htex+      dependencies:+        lambda.png:+          type: other+        code/demo.hs:+          type: other+  ```++  See [examples/example05](https://github.com/NCrashed/pdf-slave/tree/master/examples/template05) for full example.++### Input propagation++When you work with dependencies you have two options how to handle inputs:++* Define dependency inputs in its own file:++  ``` YAML+  dependencies:+    dep1:+      type:  template+      name:  dep1+      input: dep1_input.json # private input file+      body:  dep1.htex+  ```++* Define dependency inputs in parent file:++  ``` YAML+  name:  template06+  input: template06_input.json # contains inputs for dep1+  body:  template06.htex+  dependencies:+    dep1:+      type:  template+      name:  dep1+      body:  dep1.htex+  ```++  Contents of `template06_input.json`:++  ``` JSON+  {+    "dep1": {+      "line-width": 2,+      "spiral-precision": 0.01,+      "spiral-interval": [0,4],+      "spiral-a": 0.1,+      "spiral-b": 4+    }+  }+  ```++  Note that key of subsection must be equal to name of dependency.++  See [examples/example06](https://github.com/NCrashed/pdf-slave/tree/master/examples/template06) for full example.++## Making bundles++One can pack all `.htex`, `.json`, `.yaml` and all dependencies in single YAML+bundle that can be easily distributed, transmitted between services and stored:++``` bash+cd examples/template01+pdf-slave --template template01.yaml --output template01_bundle.yaml pack+```++As modification of such bundles isn't handy, one can unpack bundle:++``` bash+pdf-slave --template template01_bundle.yaml --output template01_directory unpack+```++Rendering of bundles is handled with the same command that is used for ordinary+templates.++Compilation+===========++You need:++* LaTeX distribution (for instance, texlive or miktex)++* [stack](https://docs.haskellstack.org/en/stable/README/) or system wide GHC 8.0.1 + Cabal 1.24.0.++Compilation with stack:+``` bash+git clone https://github.com/NCrashed/pdf-slave.git+cd pdf-slave+stack install+```++Compilation with cabal:+``` bash+git clone https://github.com/NCrashed/pdf-slave.git+cd pdf-slave+cabal sandbox init+cabal install --dependencies-only+cabal install+```++Running examples+================++You need:++* LaTeX distribution (for instance, texlive or miktex)++* stack or GHC+Cabal (yes, you need GHC to evaluate templates at runtime)++* `pdf-slave` and `haskintex` executables in PATH++Stack users:+``` bash+cd examples/template01+stack install aeson HaTeX+stack exec -- pdf-slave --template template01.yaml --output output.pdf pdf && xdg-open output.pdf+```++Cabal users:+``` bash+cd examples/template01+cabal sandbox init+cabal install aeson HaTeX+pdf-slave --template template01.yaml --output output.pdf pdf && xdg-open output.pdf+```++Docker build+============++1. Run `cook_doocker.sh` script. This will build two images, one `pdf-slave-build` for compilation of binaries and the second one `pdf-slave` for production usage.++2. Usage:++  ``` bash+  docker run -it --rm -v $(pwd)/examples/template01:/data/examples pdf-slave pdf --template examples/template01.yaml --output examples/output.pdf+  xdg-open examples/template01/output.pdf+  ```++3. Or download [precompiled container](https://hub.docker.com/r/ncrashed/pdf-slave/) from Docker Hub.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -0,0 +1,149 @@+module Main where++import Control.Monad.IO.Class+import Data.Maybe (fromMaybe)+import Data.Monoid+import Data.Text (pack)+import Data.Yaml (decodeEither', encode)+import Filesystem.Path.CurrentOS (directory)+import Options.Applicative as O+import Prelude hiding (FilePath)+import Shelly++import qualified Data.Aeson as A+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as BZ++import Text.PDF.Slave.Template+import Text.PDF.Slave.Render++-- | Define actions that CLI can do+data Command =+  -- | Usual mode, render PDF for template+    GeneratePDF+  -- | Make a all-in bundle from template distributed over files+  | PackBundle+  -- | Make a template distributed over files from all-in bundle+  | UnpackBundle++-- | CLI options+data Options = Options {+  -- | Path to template file, if missing, stdin is used+  templatePath  :: Maybe FilePath+  -- | Path to output pdf file, if missing, stdout is used+, pdfOutputPath :: Maybe FilePath+  -- | Path to JSON input file for overwritting input data of bundle+, inputOverwritePath :: Maybe FilePath+  -- | CLI action+, cliCommand    ::  Command+}++-- | Same as 'strOption' but parses Text+filePathOption :: Mod OptionFields String -> Parser FilePath+filePathOption m = fmap (fromText . pack) $ strOption m++-- | Parser of CLI options+optionsParser :: Parser Options+optionsParser = Options+  <$> optional (filePathOption $+       long "template"+    <> help "Path to template or bundle YAML description, if missing stdin is used"+    <> metavar "TEMPLATE_YAML_PATH"+    )+  <*> optional (filePathOption $+       long "output"+    <> help "Path to output PDF file, if missing stdout is used"+    <> metavar "OUTPUT_PDF_PATH"+    )+  <*> optional (filePathOption $+       long "input"+    <> help ("You can specify JSON file as input that will overwrite default input"+      <> " for bundle or template input file" )+    <> metavar "INPUT_JSON_PATH"+    )+  <*> commandParser++-- | Parser of CLI commands+commandParser :: Parser Command+commandParser = subparser $+     (O.command "pdf" $ info (pure GeneratePDF) $ progDesc "Generate PDF from template")+  <> (O.command "pack" $ info (pure PackBundle) $ progDesc "Construct all-in bundle from template distributed over files")+  <> (O.command "unpack" $ info (pure UnpackBundle) $ progDesc "Deconstruct all-in bundle to several files")++-- | Execute PDF slave+pdfSlave :: Options -> IO ()+pdfSlave Options{..} = shelly $ do+  -- define base directory+  baseDir <- case templatePath of+    Nothing -> pwd+    Just p -> canonic $ directory p+  -- read template+  (templateContent, contentFilename) <- case templatePath of+    Nothing -> do+      cnt <- liftIO $ BS.getContents+      return (cnt, fromText "<stdin>")+    Just p -> (,p) <$> readBinary p+  -- read optional input overwrite+  optInput <- case inputOverwritePath of+    Nothing -> return Nothing+    Just p -> do+      cnt <- readBinary p+      case A.eitherDecode' . BZ.fromStrict $ cnt of+        Left e -> fail $ "Failed to read input overwrite file: " <> show e+        Right i -> return $ Just i+  -- process particular CLI action+  case cliCommand of+    -- Generate PDF from template or bundle+    GeneratePDF -> do+      -- parse template contents+      res <- parseBundleOrTemplate contentFilename templateContent+      -- render+      bs <- case res of+        Left bundle -> do+          let bundle' = fromMaybe bundle $ (\i -> bundle { templateInput = i }) <$> optInput+          renderBundleToPDF bundle' baseDir+        Right template -> do+          let template' = fromMaybe template $+                (const $ template { templateFileInput = inputOverwritePath}) <$> optInput+          renderTemplateToPDF template' baseDir+      -- output results+      case pdfOutputPath of+        Nothing -> liftIO $ BS.putStr bs+        Just outputPath -> writeBinary outputPath bs++    -- Pack bundle from distrubuted template format+    PackBundle -> case decodeEither' templateContent of+      Left e -> fail $ "Failed to parse template: " <> show e+      Right tf -> do+        res <- loadTemplateInMemory tf+        case res of+          Left e -> fail $ "Failed to pack bundle: " <> show e+          Right t -> do+            let bs = encode t+            case pdfOutputPath of+              Nothing -> liftIO $ BS.putStr bs+              Just outputPath -> do+                writeBinary outputPath bs+                outputPath' <- toTextWarn outputPath+                echo $ "Bundle is written to " <> outputPath'++    -- Unpack bundle to distributed template format+    UnpackBundle -> case decodeEither' templateContent of+      Left e -> fail $ "Failed to parse template: " <> show e+      Right t -> case pdfOutputPath of+        Nothing -> fail "Expecting --output parameter as destination folder"+        Just outputFolder -> do+          mkdir_p outputFolder+          tf <- storeTemplateInFiles t outputFolder+          let templateFilename = outputFolder </> templateName t <.> "yaml"+          writeBinary templateFilename $ encode tf+          outputFolder' <- toTextWarn outputFolder+          echo $ "Bundle is unpacked to " <> outputFolder'++main :: IO ()+main = execParser opts >>= pdfSlave+  where+    opts = info (helper <*> optionsParser)+      ( fullDesc+     <> progDesc "CLI interface for PDF slave."+     <> header "pdf-slave - tool for building PDF documents from HTex templates" )
+ pdf-slave.cabal view
@@ -0,0 +1,76 @@+name:                pdf-slave+version:             1.0.0.0+synopsis:            Tool to generate PDF from haskintex templates and YAML input+description:         Please see README.md+homepage:            https://github.com/NCrashed/pdf-slave#readme+license:             BSD3+license-file:        LICENSE+author:              Anton Gushcha+maintainer:          ncrashed@gmail.com+copyright:           2016 Anton Gushcha+category:            Web+build-type:          Simple+cabal-version:       >=1.10+extra-source-files:+  README.md+  CHANGELOG.md+  stack.yaml++library+  hs-source-dirs:      src+  exposed-modules:+    Text.PDF.Slave+    Text.PDF.Slave.Render+    Text.PDF.Slave.Template+  default-language:    Haskell2010+  build-depends:+      base                     >= 4.7      && < 5+    , aeson                    >= 0.11     && < 0.12+    , base64-bytestring        >= 1.0      && < 1.1+    , bytestring               >= 0.10     && < 0.11+    , containers               >= 0.5      && < 0.6+    , exceptions               >= 0.8      && < 0.9+    , haskintex                >= 0.6      && < 0.8+    , HaTeX                    >= 3.16     && < 3.18+    , shelly                   >= 1.6      && < 1.7+    , system-filepath          >= 0.4      && < 0.5+    , text                     >= 1.2      && < 1.3+    , unordered-containers     >= 0.2      && < 0.3+    , yaml                     >= 0.8      && < 0.9++  default-extensions:+    BangPatterns+    DeriveGeneric+    FlexibleContexts+    FlexibleInstances+    FunctionalDependencies+    GADTs+    GeneralizedNewtypeDeriving+    LambdaCase+    MultiParamTypeClasses+    OverloadedStrings+    RecordWildCards+    ScopedTypeVariables+    StandaloneDeriving+    TupleSections+    TypeFamilies++executable pdf-slave+  hs-source-dirs:      app+  main-is:             Main.hs+  default-language:    Haskell2010+  build-depends:+      base                    >= 4.7      && < 5+    , aeson                   >= 0.11     && < 0.12+    , bytestring              >= 0.10     && < 0.11+    , optparse-applicative    >= 0.12     && < 0.13+    , pdf-slave+    , shelly                  >= 1.6      && < 1.7+    , system-filepath         >= 0.4      && < 0.5+    , text                    >= 1.2      && < 1.3+    , yaml                    >= 0.8      && < 0.9++  default-extensions:+    OverloadedStrings+    RecordWildCards+    TupleSections
+ src/Text/PDF/Slave.hs view
@@ -0,0 +1,23 @@+-- | Reexports+module Text.PDF.Slave(+  -- * Template definition+    TemplateName+  , TemplateInput+  , TemplateBody+  , TemplateBibtex+  , DependencyBody+  , BibTexBody+  , TemplateDependency(..)+  , Template(..)+  , TemplateDependencyFile(..)+  , TemplateFile(..)+  -- ** Bundle helpers+  , loadTemplateInMemory+  , storeTemplateInFiles+  -- * Template rendering to PDF+  , PDFContent+  , renderTemplateToPDF+  ) where++import Text.PDF.Slave.Render+import Text.PDF.Slave.Template
+ src/Text/PDF/Slave/Render.hs view
@@ -0,0 +1,241 @@+-- | Rendering of templates+module Text.PDF.Slave.Render(+    PDFContent+  , PDFRenderException(..)+  , displayPDFRenderException+  , renderBundleOrTemplateFromFile+  , renderFromFileBundleToPDF+  , renderFromFileToPDF+  , renderBundleToPDF+  , renderTemplateToPDF+  -- * Low-level+  , DepFlags+  , DepFlag(..)+  , renderPdfTemplate+  , renderTemplate+  , renderTemplateDep+  , parseBundleOrTemplate+  , parseBundleOrTemplateFromFile+  ) where++import Control.Monad (join)+import Control.Monad.Catch+import Data.Aeson (Value(..))+import Data.ByteString (ByteString)+import Data.Maybe (fromMaybe)+import Data.Monoid+import Data.Set (Set)+import Data.Yaml (ParseException, decodeEither')+import Filesystem.Path (dropExtension, directory)+import GHC.Generics+import Prelude hiding (FilePath)+import Shelly++import qualified Data.Aeson as A+import qualified Data.ByteString.Lazy as BZ+import qualified Data.Foldable as F+import qualified Data.HashMap.Strict as H+import qualified Data.Map.Strict as M+import qualified Data.Set as S++import Text.PDF.Slave.Template++-- | Contents of PDF file+type PDFContent = ByteString++-- | Errors that are thrown by rendering functions+data PDFRenderException =+    TemplateFormatError FilePath ParseException -- ^ Failed to parse template YAML+  | BundleFormatError FilePath ParseException -- ^ Failed to parse template bundle YAML+  -- | Failed to parse file in both formats: bundle and template file.+  | BundleOrTemplateFormatError FilePath ParseException ParseException+  | InputFileFormatError FilePath String -- ^ Failed to parse JSON input+  deriving (Generic, Show)++instance Exception PDFRenderException++-- | Convert PDF rendering exception to user readable format+displayPDFRenderException :: PDFRenderException -> String+displayPDFRenderException e = case e of+  TemplateFormatError f pe -> "Failed to parse template file " <> show f+    <> ", reason: " <> show pe+  BundleFormatError f pe -> "Failed to parse template bundle file " <> show f+    <> ", reason: " <> show pe+  BundleOrTemplateFormatError f peBundle peTemplate -> "Failed to parse template file " <> show f <> ". "+    <> "\n Tried bundle format: " <> show peBundle+    <> "\n Tried template format: " <> show peTemplate+  InputFileFormatError f es -> "Failed to parse template input file " <> show f+    <> ", reason: " <> show es++-- | Helper to render either a bundle or distributed template from file to PDF.+renderBundleOrTemplateFromFile ::+     FilePath -- ^ Path to either bundle 'Template' or template 'TemplateFile'+  -> Maybe Value -- ^ Overwrite of input JSON for bundle+  -> Sh PDFContent+renderBundleOrTemplateFromFile filename bundleInput = do+  res <- parseBundleOrTemplateFromFile filename+  let baseDir = directory filename+  case res of+    Left bundle -> do+      let bundle' = fromMaybe bundle $ fmap (\i -> bundle { templateInput = Just i }) bundleInput+      renderBundleToPDF bundle' baseDir+    Right template -> renderTemplateToPDF template baseDir++-- | Try to parse either a bundle or template file+parseBundleOrTemplateFromFile :: FilePath -- ^ Path to either 'Template' or 'TemplateFile'+  -> Sh (Either Template TemplateFile)+parseBundleOrTemplateFromFile filename =+  parseBundleOrTemplate filename =<< readBinary filename++-- | Try to parse either a bundle or template file+parseBundleOrTemplate :: FilePath -- ^ Source of data (file or stdin, etc)+  -> ByteString -- ^ Contents of either 'Template' or 'TemplateFile'+  -> Sh (Either Template TemplateFile)+parseBundleOrTemplate filename cnt = case decodeEither' cnt of+  Right bundle -> return $ Left bundle+  Left eBundle -> case decodeEither' cnt of+    Right template -> return $ Right template+    Left eTemplate -> throwM $ BundleOrTemplateFormatError filename eBundle eTemplate++-- | Helper to render from all-in bundle template+renderFromFileBundleToPDF :: FilePath -- ^ Path to 'Template' all-in bundle+  -> Maybe Value -- ^ Overwrite of input JSON for bundle+  -> Sh PDFContent+renderFromFileBundleToPDF filename bundleInput = do+  cnt <- readBinary filename+  case decodeEither' cnt of+    Left e -> throwM $ BundleFormatError filename e+    Right bundle -> do+      let bundle' = fromMaybe bundle $ fmap (\i -> bundle { templateInput = Just i }) bundleInput+      renderBundleToPDF bundle' (directory filename)++-- | Helper to render from template file+renderFromFileToPDF :: FilePath -- ^ Path to 'TemplateFile'+  -> Sh PDFContent+renderFromFileToPDF filename = do+  cnt <- readBinary filename+  case decodeEither' cnt of+    Left e -> throwM $ TemplateFormatError filename e+    Right template -> renderTemplateToPDF template (directory filename)++-- | Unpack bundle, render the template, cleanup and return PDF+renderBundleToPDF :: Template -- ^ Input all-in template+  -> FilePath -- ^ Base directory+  -> Sh PDFContent+renderBundleToPDF bundle baseDir = withTmpDir $ \unpackDir -> do+  template <- storeTemplateInFiles bundle unpackDir+  renderTemplateToPDF template baseDir++-- | Render template and return content of resulted PDF file+renderTemplateToPDF :: TemplateFile -- ^ Input template+  -> FilePath -- ^ Base directory+  -> Sh PDFContent -- ^ Output PDF file+renderTemplateToPDF t@TemplateFile{..} baseDir = withTmpDir $ \outputFolder -> do+  -- Parse global input file and pass it as inherited input+  minput <- case templateFileInput of+    Nothing -> return Nothing+    Just inputName -> do+      cnt <- readBinary inputName+      case A.eitherDecode' . BZ.fromStrict $ cnt of+        Left e -> throwM $ InputFileFormatError inputName e+        Right a -> return $ Just a+  renderPdfTemplate minput t baseDir outputFolder+  readBinary (outputFolder </> templateFileName <.> "pdf")++-- | Low-level render of template from .htex to .pdf that is recursively used for dependencies+renderPdfTemplate :: Maybe Value -- ^ Inherited input from parent+  -> TemplateFile -- ^ Template to render+  -> FilePath -- ^ Base directory+  -> FilePath -- ^ Output folder+  -> Sh ()+renderPdfTemplate minput t@TemplateFile{..} baseDir outputFolder = do+  flags <- renderTemplate minput t baseDir outputFolder+  -- define commands of compilation pipe+  let pdflatex = bash "pdflatex" [+          "-synctex=1"+        , "-interaction=nonstopmode"+        , toTextArg $ templateFileName <.> "tex" ]+      bibtex = bash "bibtex" [+          toTextArg $ templateFileName <.> "aux" ]+  -- read flags and construct pipe+  chdir outputFolder $ do+    _ <- if S.member NeedBibtex flags+      then pdflatex -|- bibtex -|- pdflatex -|- pdflatex+      else pdflatex+    return ()++-- | Low-level render of template from .htex to .tex that is recursively used for dependencies+renderTemplate :: Maybe Value -- ^ Inherited input from parent+  -> TemplateFile -- ^ Template to render+  -> FilePath -- ^ Base directory+  -> FilePath -- ^ Output folder+  -> Sh DepFlags -- ^ Flags that affects compilation upper in the deptree+renderTemplate minput TemplateFile{..} baseDir outputFolder = do+  mkdir_p outputFolder+  let renderDepenency = renderTemplateDep minput baseDir outputFolder+  depFlags <- M.traverseWithKey renderDepenency templateFileDeps+  let+      bodyName = dropExtension templateFileBody+      haskintex = bash "haskintex" $ [+          "-overwrite"+        , "-verbose"+        , toTextArg $ baseDir </> bodyName ]+        ++ templateFileHaskintexOpts+  -- input file might be missing, if missing we can inject input from parent+  case templateFileInput of+    Nothing -> whenJust minput $ \input -> do+      let filename = outputFolder </> (templateFileName <> "_input") <.> "json"+      writeBinary filename $ BZ.toStrict . A.encode $ input+    Just inputName -> do+      let inputPath = baseDir </> inputName+      cp inputPath $ outputFolder </> inputName+  _ <- chdir outputFolder haskintex+  return $ F.foldMap id depFlags -- merge flags++-- | Collected dependency markers (for instance, that we need bibtex compilation)+type DepFlags = Set DepFlag++-- | Dependency marker that is returned from 'renderTemplateDep'+data DepFlag = NeedBibtex -- ^ We need a bibtex compliation+  deriving (Generic, Show, Ord, Eq)++-- | Render template dependency+renderTemplateDep :: Maybe Value -- ^ Inherited input from parent+  -> FilePath -- ^ Base directory+  -> FilePath  -- ^ Output folder+  -> TemplateName -- ^ Dependency name+  -> TemplateDependencyFile -- ^ Dependency type+  -> Sh DepFlags+renderTemplateDep minput baseDir outputFolder name dep = case dep of+  BibtexDepFile -> do+    let file = fromText name+        outputFile = outputFolder </> file+    mkdir_p (directory outputFile)+    cp (baseDir </> file) outputFile+    return $ S.singleton NeedBibtex+  TemplateDepFile template -> do+    let subFolder = baseDir </> fromText name+        outputSubFolder = outputFolder </> fromText name+    renderTemplate minput' template subFolder outputSubFolder+  TemplatePdfDepFile template -> do+    let subFolder = baseDir </> fromText name+        outputSubFolder = outputFolder </> fromText name+    renderPdfTemplate minput' template subFolder outputSubFolder+    return mempty+  OtherDepFile -> do+    let file = fromText name+        outputFile = outputFolder </> file+    mkdir_p (directory outputFile)+    cp (baseDir </> file) outputFile+    return mempty+  where+    -- Try to find subsection in input that refer to the dependency+    minput' :: Maybe Value+    minput' = join $ flip fmap minput $ \case+      Object o -> H.lookup name o+      _ -> Nothing++-- | Same as 'when', but for 'Just' values.+whenJust :: Applicative f => Maybe a -> (a -> f ()) -> f ()+whenJust Nothing _ = pure ()+whenJust (Just a) f = f a
+ src/Text/PDF/Slave/Template.hs view
@@ -0,0 +1,291 @@+-- | Defines document template+{-# OPTIONS_GHC -fno-warn-orphans #-}+module Text.PDF.Slave.Template(+    TemplateName+  , TemplateInput+  , TemplateBody+  , TemplateBibtex+  , DependencyBody+  , BibTexBody+  , TemplateDependency(..)+  , Template(..)+  , TemplateDependencyFile(..)+  , TemplateFile(..)+  -- * Helpers+  , loadTemplateInMemory+  , storeTemplateInFiles+  ) where++import Control.Monad (mzero)+import Data.ByteString (ByteString)+import Data.Monoid+import Data.Text as T+import Data.Yaml+import Filesystem.Path.CurrentOS (directory)+import GHC.Generics+import Prelude hiding (FilePath)+import Shelly as Sh++import qualified Data.Aeson             as A+import qualified Data.ByteString.Base64 as B64+import qualified Data.ByteString.Lazy   as BZ+import qualified Data.Map.Strict        as M+import qualified Data.Text.Encoding     as T++-- | Template unique name+type TemplateName = Text++-- | A template takes simple YAML document as input+type TemplateInput = Value++-- | Template body is text with .htex content+type TemplateBody = Text++-- | Template can define additional bibtex database+type TemplateBibtex = Text++-- | Dependency can be a binary file+type DependencyBody = ByteString++-- | Content of bibtex file+type BibTexBody = Text++-- | Template has different types of dependencies, each type of the dependecy+-- has own affect on rendering pipe.+data TemplateDependency =+  -- | Bibtex file for references to other documents. Need call to bibtex+    BibtexDep BibTexBody+  -- | HTex file that need to be compiled to .tex file+  | TemplateDep Template+  -- | HTex file that need to be compiled to .pdf file+  | TemplatePdfDep Template+  -- | Any other file that doesn't need a compilation (listings, images, etc)+  | OtherDep DependencyBody+  deriving (Generic, Show)++instance FromJSON TemplateDependency where+  parseJSON val@(Object o) = do+    depType <- o .: "type"+    case T.toLower . T.strip $ depType of+      "bibtex"       -> BibtexDep       <$> o .: "body"+      "template"     -> TemplateDep     <$> parseJSON val+      "template_pdf" -> TemplatePdfDep  <$> parseJSON val+      "other"        -> do+        (t :: Text) <- o .: "body"+        either (\e -> fail $ "Cannot decode dependency body (base64): " <> e) (return . OtherDep) $+          B64.decode . T.encodeUtf8 $ t+      _ -> fail $ "Unknown template type " <> unpack depType+  parseJSON _ = mzero++instance ToJSON TemplateDependency where+  toJSON d = case d of+    BibtexDep body -> object [+        "type" .= ("bibtex" :: Text)+      , "body" .= body+      ]+    TemplateDep body -> let+      Object o1 = object [ "type" .= ("template" :: Text) ]+      Object o2 = toJSON body+      in Object (o1 <> o2)+    TemplatePdfDep body -> let+      Object o1 = object [ "type" .= ("template_pdf" :: Text) ]+      Object o2 = toJSON body+      in Object (o1 <> o2)+    OtherDep bs -> object [+        "type" .= ("other" :: Text)+      , "body" .= (T.decodeUtf8 . B64.encode $ bs)+      ]++-- | Description of document template+data Template = Template {+  -- | Template has human readable name+    templateName          :: TemplateName+  -- | Template expects input in YAML format+  , templateInput         :: Maybe TemplateInput+  -- | Template contents+  , templateBody          :: TemplateBody+  -- | Template dependencies (bibtex, listings, other htex files)+  , templateDeps          :: M.Map TemplateName TemplateDependency+  -- | Additional flags for `haskintex`+  , templateHaskintexOpts :: [Text]+  } deriving (Generic, Show)++instance FromJSON Template where+  parseJSON (Object o) = Template+    <$> o .: "name"+    <*> o .:? "input"+    <*> o .: "body"+    <*> o .:? "dependencies" .!= mempty+    <*> o .:? "haskintex-opts" .!= mempty+  parseJSON _ = mzero++instance ToJSON Template where+  toJSON Template{..} = object [+      "name"           .= templateName+    , "input"          .= templateInput+    , "body"           .= templateBody+    , "dependencies"   .= templateDeps+    , "haskintex-opts" .= templateHaskintexOpts+    ]++-- | Same as 'TemplateDependency' but keeps contents in separate files+data TemplateDependencyFile =+  -- | Bibtex file for references to other documents. Need call to bibtex.+  -- Name of dependency is a filename with contents.+    BibtexDepFile+  -- | HTex file that need to be compiled to .tex file+  -- Name of dependency defines a subfolder for the template.+  | TemplateDepFile TemplateFile+  -- | HTex file that need to be compiled to .pdf file+  -- Name of dependency deinfes a subfolder for the template.+  | TemplatePdfDepFile TemplateFile+  -- | Any other file that doesn't need a compilation (listings, images, etc)+  -- Name of dependency is a filename with contents.+  | OtherDepFile+  deriving (Generic, Show)++instance FromJSON FilePath where+  parseJSON (String s) = return $ Sh.fromText s+  parseJSON _ = mzero++instance ToJSON FilePath where+  toJSON = String . Sh.toTextIgnore++instance FromJSON TemplateDependencyFile where+  parseJSON val@(Object o) = do+    depType <- o .: "type"+    case T.toLower . T.strip $ depType of+      "bibtex"       -> pure BibtexDepFile+      "template"     -> TemplateDepFile     <$> parseJSON val+      "template_pdf" -> TemplatePdfDepFile  <$> parseJSON val+      "other"        -> pure OtherDepFile+      _ -> fail $ "Unknown template type " <> unpack depType+  parseJSON _ = mzero++instance ToJSON TemplateDependencyFile where+  toJSON d = case d of+    BibtexDepFile -> object [+        "type" .= ("bibtex" :: Text)+      ]+    TemplateDepFile body -> let+      Object o1 = object [ "type" .= ("template" :: Text) ]+      Object o2 = toJSON body+      in Object (o1 <> o2)+    TemplatePdfDepFile body -> let+      Object o1 = object [ "type" .= ("template_pdf" :: Text) ]+      Object o2 = toJSON body+      in Object (o1 <> o2)+    OtherDepFile -> object [+        "type" .= ("other" :: Text)+      ]++-- | Same as 'Template', but holds info about template content and dependencies+-- in other files.+data TemplateFile = TemplateFile {+  -- | Template has human readable name+    templateFileName      :: TemplateName+  -- | Template expects input in JSON format. The field contains filename of the YAML file.+  , templateFileInput     :: Maybe FilePath+  -- | Template contents filename.+  , templateFileBody      :: FilePath+  -- | Template dependencies (bibtex, listings, other htex files)+  , templateFileDeps      :: M.Map TemplateName TemplateDependencyFile+  -- | Additional flags for `haskintex`+  , templateFileHaskintexOpts :: [Text]+  } deriving (Generic, Show)++instance FromJSON TemplateFile where+  parseJSON (Object o) = TemplateFile+    <$> o .: "name"+    <*> o .:? "input"+    <*> o .: "body"+    <*> o .:? "dependencies" .!= mempty+    <*> o .:? "haskintex-opts" .!= mempty+  parseJSON _ = mzero++instance ToJSON TemplateFile where+  toJSON TemplateFile{..} = object [+      "name"           .= templateFileName+    , "input"          .= templateFileInput+    , "body"           .= templateFileBody+    , "dependencies"   .= templateFileDeps+    , "haskintex-opts" .= templateFileHaskintexOpts+    ]++-- | Load all external references of template into memory+loadTemplateInMemory :: TemplateFile -> Sh (Either String Template)+loadTemplateInMemory TemplateFile{..} = do+  inputCnt <- case templateFileInput of+    Nothing -> return $ Right Nothing+    Just fname -> do+      cnt <- readBinary fname+      return $ fmap Just . A.eitherDecode' . BZ.fromStrict $ cnt+  body <- readfile templateFileBody+  deps <- M.traverseWithKey loadDep templateFileDeps+  return $ Template+    <$> pure templateFileName+    <*> inputCnt+    <*> pure body+    <*> sequence deps+    <*> pure templateFileHaskintexOpts+  where+    loadDep name d = let+      filename = fromText name+      in case d of+        BibtexDepFile -> do+          cnt <- readfile filename+          return . pure $ BibtexDep cnt+        TemplateDepFile body -> do+          tmpl <- chdir filename $ loadTemplateInMemory body+          return $ TemplateDep <$> tmpl+        TemplatePdfDepFile body -> do+          tmpl <- chdir filename $ loadTemplateInMemory body+          return $ TemplatePdfDep <$> tmpl+        OtherDepFile -> do+          cnt <- readBinary filename+          return . pure $ OtherDep cnt++-- | Extract all external references of template into file system+storeTemplateInFiles :: Template -> FilePath -> Sh TemplateFile+storeTemplateInFiles Template{..} folder = do+  mkdir_p folder+  relInputName <- case templateInput of+    Nothing -> return Nothing+    Just input -> do+      let inputName = folder </> (templateName <> "_input") <.> "json"+      writeBinary inputName $ BZ.toStrict $ A.encode input+      fmap Just $ relativeTo folder inputName+  let bodyName = folder </> templateName <.> "htex"+  mkdir_p $ directory bodyName+  writefile bodyName templateBody+  relBodyName <- relativeTo folder bodyName+  deps <- M.traverseWithKey storeDep templateDeps+  return $ TemplateFile {+      templateFileName = templateName+    , templateFileInput = relInputName+    , templateFileBody = relBodyName+    , templateFileDeps = deps+    , templateFileHaskintexOpts = templateHaskintexOpts+    }+  where+    storeDep name d = case d of+      BibtexDep body -> do+        let bodyName = folder </> name+        mkdir_p $ directory bodyName+        writefile bodyName body+        return BibtexDepFile+      TemplateDep template -> do+        let subfolderName = folder </> Sh.fromText name+        mkdir_p subfolderName+        dep <- storeTemplateInFiles template subfolderName+        return $ TemplateDepFile dep+      TemplatePdfDep template -> do+        let subfolderName = folder </> Sh.fromText name+        mkdir_p subfolderName+        dep <- storeTemplateInFiles template subfolderName+        return $ TemplatePdfDepFile dep+      OtherDep body -> do+        let bodyName = folder </> name+        mkdir_p $ directory bodyName+        writeBinary bodyName body+        return OtherDepFile
+ stack.yaml view
@@ -0,0 +1,67 @@+# 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:+# http://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+# resolver: ghcjs-0.1.0_ghc-7.10.2+# resolver:+#  name: custom-snapshot+#  location: "./custom-snapshot.yaml"+resolver: lts-7.14++# 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+#    commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a+# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a+#   extra-dep: true+#  subdirs:+#  - auto-update+#  - wai+#+# A package marked 'extra-dep: true' will only be built if demanded by a+# non-dependency (i.e. a user package), and its test suites and benchmarks+# will not be run. This is useful for tweaking upstream packages.+packages:+- '.'++# Dependency packages to be pulled from upstream that are not in the resolver+# (e.g., acme-missiles-0.3)+extra-deps: []++# 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.3"+#+# 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