{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE PolyKinds #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE LambdaCase #-} {-# OPTIONS_GHC -fwarn-incomplete-patterns #-} {-| Module : Knit.Effect.Pandoc Description : Polysemy writer-like effect Copyright : (c) Adam Conner-Sax 2019 License : BSD-3-Clause Maintainer : adam_conner_sax@yahoo.com Stability : experimental Polysemy Pandoc effect. This is writer-like, allowing the interspersed addition of various Pandoc-readable formats into one doc and then rendering to many Pandoc-writeable formats. Currently only a subset of formats are supported. Inputs can express requirements, e.g., hvega requires html output because it uses javascript. Those requirements are then checked before output is rendered and an error thrown if the input is not supported. -} module Knit.Effect.Pandoc ( -- * Effects ToPandoc , FromPandoc -- * Requirement Support , Requirement(..) , PandocWithRequirements -- * Format ADTs , PandocReadFormat(..) , PandocWriteFormat(..) -- * Combinators , addFrom , require , writeTo , toPandoc , fromPandoc -- * Interpreters , runPandocWriter -- * Docs effect type-aliases , Pandocs -- * Pandoc Specific Info , PandocInfo(..) -- * Docs Effect Interpreters , newPandoc , pandocsToDocs , fromPandocE -- * Re-exports , DocWithInfo(..) ) where import qualified Text.Pandoc as PA import qualified Data.Text as T import Data.ByteString.Lazy as LBS import qualified Data.Foldable as F import qualified Data.Map as M import qualified Data.Monoid as Mon import Data.Set as S import qualified Text.Blaze.Html as Blaze import Control.Monad.Except ( throwError ) import qualified Polysemy as P import Polysemy.Internal ( send ) import qualified Polysemy.Writer as P import qualified Knit.Effect.PandocMonad as PM import Knit.Effect.Docs ( Docs , DocWithInfo(..) , newDoc , toDocList ) -- For now, just handle the Html () case since then it's monoidal and we can interpret via writer --newtype FreerHtml = FreerHtml { unFreer :: H.Html () } -- | Supported formats for adding to current Pandoc data PandocReadFormat a where ReadDocX :: PandocReadFormat LBS.ByteString ReadMarkDown :: PandocReadFormat T.Text ReadCommonMark :: PandocReadFormat T.Text ReadRST :: PandocReadFormat T.Text ReadLaTeX :: PandocReadFormat T.Text ReadHtml :: PandocReadFormat T.Text deriving instance Show (PandocReadFormat a) -- | Supported formats for writing current Pandoc data PandocWriteFormat a where WriteDocX :: PandocWriteFormat LBS.ByteString WriteMarkDown :: PandocWriteFormat T.Text WriteCommonMark :: PandocWriteFormat T.Text WriteRST :: PandocWriteFormat T.Text WriteLaTeX :: PandocWriteFormat T.Text WriteHtml5 :: PandocWriteFormat Blaze.Html -- Blaze WriteHtml5String :: PandocWriteFormat T.Text deriving instance Show (PandocWriteFormat a) -- | ADT to allow inputs to request support, if necessary or possible, in the output format. -- E.g., Latex output in Html needs MathJax. But Latex needs to nothing to output in Latex. -- Vega-lite needs some script headers to output in Html and can't be output in other formats. -- For now, we support all the things we can in any output format so this just results -- in a runtime test. -- TODO (?): Allow headers/extensions to be added/switched based on this. data Requirement = VegaSupport -- ^ Supported only for Html output. | LatexSupport -- ^ Supported in Html output (via MathJax) and Latex output. deriving (Show, Ord, Eq, Bounded, Enum) handlesAll :: PandocWriteFormat a -> S.Set Requirement -> Bool handlesAll f rs = Mon.getAll $ F.fold (fmap (Mon.All . handles f) $ S.toList rs) where handles :: PandocWriteFormat a -> Requirement -> Bool handles WriteHtml5 VegaSupport = True handles WriteHtml5String VegaSupport = True handles WriteHtml5 LatexSupport = True handles WriteHtml5String LatexSupport = True handles WriteLaTeX LatexSupport = True handles _ _ = False data PandocWithRequirements = PandocWithRequirements { doc :: PA.Pandoc, reqs :: S.Set Requirement } instance Semigroup PandocWithRequirements where (PandocWithRequirements da ra) <> (PandocWithRequirements db rb) = PandocWithRequirements (da <> db) (ra <> rb) instance Monoid PandocWithRequirements where mempty = PandocWithRequirements mempty mempty justDoc :: PA.Pandoc -> PandocWithRequirements justDoc d = PandocWithRequirements d mempty justRequirement :: Requirement -> PandocWithRequirements justRequirement r = PandocWithRequirements mempty (S.singleton r) -- | Pandoc writer, add any read format to current doc data ToPandoc m r where AddFrom :: PandocReadFormat a -> PA.ReaderOptions -> a -> ToPandoc m () -- ^ add to current doc Require :: Requirement -> ToPandoc m () -- ^ require specific support -- | Pandoc output effect, take given doc and produce formatted output data FromPandoc m r where WriteTo :: PandocWriteFormat a -> PA.WriterOptions -> PA.Pandoc -> FromPandoc m a -- convert to given format -- | Add a piece of a Pandoc readable type to the current doc addFrom :: P.Member ToPandoc effs => PandocReadFormat a -> PA.ReaderOptions -> a -> P.Sem effs () addFrom prf pro doc' = send $ AddFrom prf pro doc' -- | Add a requirement that the output format must satisfy. require :: P.Member ToPandoc effs => Requirement -> P.Sem effs () require r = send $ Require r -- | Write given doc in requested format writeTo :: P.Member FromPandoc effs => PandocWriteFormat a -> PA.WriterOptions -> PA.Pandoc -> P.Sem effs a writeTo pwf pwo pdoc = send $ WriteTo pwf pwo pdoc -- | Convert a to Pandoc with the given options toPandoc :: PA.PandocMonad m => PandocReadFormat a -> PA.ReaderOptions -> a -> m PA.Pandoc toPandoc prf pro x = readF pro x where readF = case prf of ReadDocX -> PA.readDocx ReadMarkDown -> PA.readMarkdown ReadCommonMark -> PA.readCommonMark ReadRST -> PA.readRST ReadLaTeX -> PA.readLaTeX ReadHtml -> PA.readHtml -- | Convert Pandoc to requested format with the given options. -- | Throw a PandocError if the output format is unsupported given the inputs. fromPandoc :: PA.PandocMonad m => PandocWriteFormat a -> PA.WriterOptions -> PandocWithRequirements -> m a fromPandoc pwf pwo (PandocWithRequirements pdoc rs) = case handlesAll pwf rs of False -> throwError $ PA.PandocSomeError $ "One of " ++ (show $ S.toList rs) ++ " cannot be output to " ++ show pwf True -> write pwo pdoc where write = case pwf of WriteDocX -> PA.writeDocx WriteMarkDown -> PA.writeMarkdown WriteCommonMark -> PA.writeCommonMark WriteRST -> PA.writeRST WriteLaTeX -> PA.writeLaTeX WriteHtml5 -> PA.writeHtml5 WriteHtml5String -> PA.writeHtml5String -- | Re-interpret ToPandoc in Writer toWriter :: PM.PandocEffects effs => P.Sem (ToPandoc ': effs) a -> P.Sem (P.Writer PandocWithRequirements ': effs) a toWriter = P.reinterpret $ \case (AddFrom rf ro x) -> P.raise (fmap justDoc $ PM.absorbPandocMonad $ toPandoc rf ro x) >>= P.tell @PandocWithRequirements (Require r) -> P.tell (justRequirement r) -- | Run ToPandoc by interpreting in Writer and then running that Writer. runPandocWriter :: PM.PandocEffects effs => P.Sem (ToPandoc ': effs) () -> P.Sem effs PandocWithRequirements runPandocWriter = fmap fst . P.runWriter . toWriter -- | Type to hold info about each document that will be required for rendering and output data PandocInfo = PandocInfo { pdiName :: T.Text, pdiTemplateVars :: M.Map String String } -- | Type-alias for use with the @Docs@ effect. type Pandocs = Docs PandocInfo PandocWithRequirements -- | Add a new named Pandoc to a Pandoc Docs collection. newPandocPure :: P.Member Pandocs effs => PandocInfo -- ^ name and template variables for document -> PandocWithRequirements -- ^ document and union of all input requirements -> P.Sem effs () newPandocPure = newDoc -- | Add the Pandoc stored in the writer-style ToPandoc effect to the named docs collection with the given name. newPandoc :: (PM.PandocEffects effs, P.Member Pandocs effs) => PandocInfo -- ^ name and template variables for document -> P.Sem (ToPandoc ': effs) () -> P.Sem effs () newPandoc n l = fmap fst (P.runWriter $ toWriter l) >>= newPandocPure n -- | Given a write format and options, convert the NamedDoc to the requested format pandocFrom :: PA.PandocMonad m => PandocWriteFormat a -- ^ format for Pandoc output -> PA.WriterOptions -- ^ options for the Pandoc Writer -> DocWithInfo PandocInfo PandocWithRequirements -- ^ named Pandoc with its union of requirements -> m (DocWithInfo PandocInfo a) -- ^ document in output format (in the effects monad). pandocFrom pwf pwo (DocWithInfo i pdoc) = do doc' <- fromPandoc pwf pwo pdoc return $ DocWithInfo i doc' -- | Given a write format and options, -- convert a list of named Pandocs to a list of named docs in the requested format pandocsToDocs :: PM.PandocEffects effs => PandocWriteFormat a -- ^ format for Pandoc output -> PA.WriterOptions -- ^ options for the Pandoc Writer -> P.Sem (Pandocs ': effs) () -- ^ effects stack to be (partially) run to get documents -> P.Sem effs [DocWithInfo PandocInfo a] -- ^ documents in requested format, within the effects monad pandocsToDocs pwf pwo = (traverse (\x -> PM.absorbPandocMonad $ pandocFrom pwf pwo x) =<<) . toDocList -- | Given a write format and options, run the writer-style ToPandoc effect and produce a doc of requested type fromPandocE :: PM.PandocEffects effs => PandocWriteFormat a -- ^ format for Pandoc output -> PA.WriterOptions -- ^ options for the Pandoc Writer -> P.Sem (ToPandoc ': effs) () -- ^ effects stack to be (partially) run to get document -> P.Sem effs a -- ^ document in requested format, within the effects monad fromPandocE pwf pwo = (((\x -> PM.absorbPandocMonad $ fromPandoc pwf pwo x) . fst) =<<) . P.runWriter . toWriter