packages feed

pansite-0.2.0.0: app/PansiteApp/PandocTool.hs

{-|
Module      : PansiteApp.PandocTool
Description : Pandoc tool
Copyright   : (C) Richard Cook, 2017-2018
Licence     : MIT
Maintainer  : rcook@rcook.org
Stability   : experimental
Portability : portable
-}

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

module PansiteApp.PandocTool (pandocTool) where

import           Data.Aeson
import           Data.Aeson.Types
import qualified Data.ByteString.Lazy as BL
import           Data.Default
import           Data.List
import qualified Data.Text as Text (pack)
import qualified Data.Text.IO as Text (writeFile)
import           Pansite
import           PansiteApp.Util
import           System.FilePath
import           Text.Blaze.Html.Renderer.String
import           Text.Pandoc
                    ( Extension(..)
                    , HTMLMathMethod(..)
                    , Inline(..)
                    , WriterOptions(..)
                    , enableExtension
                    , readMarkdown
                    , readerExtensions
                    , runPure
                    , writeDocx
                    , writeHtml5
                    )
import           Text.Pandoc.Walk (walk)
import           Text.Pandoc.XML (toEntities)

data PandocSettings = PandocSettings
    { psNumberSections :: Bool
    , psVars :: [(String, String)]
    , psTemplatePath :: Maybe FilePath
    , psTableOfContents :: Bool
    , psReferenceDoc :: Maybe FilePath
    , psMathJaxEnabled :: Bool
    , psIncludeInHeaderPath :: Maybe FilePath
    , psIncludeBeforeBodyPath :: Maybe FilePath
    , psIncludeAfterBodyPath :: Maybe FilePath
    }

instance Default PandocSettings where
    def = PandocSettings False [] Nothing False Nothing False Nothing Nothing Nothing

updater :: PandocSettings -> UpdateContext -> Value -> Parser PandocSettings
updater PandocSettings{..} (UpdateContext resolveFilePath) =
    withObject "pandoc" $ \o ->
        let getFilePath key d = fmap (resolveFilePath <$>) (o .:? key .!= d)
        in PandocSettings
            <$> o .:? "number-sections" .!= psNumberSections
            <*> o .:? "vars" .!= psVars
            <*> getFilePath "template-path" psTemplatePath
            <*> o .:? "table-of-contents" .!= psTableOfContents
            <*> getFilePath "reference-doc" psReferenceDoc
            <*> o .:? "mathjax" .!= psMathJaxEnabled
            <*> getFilePath "include-in-header-path" psIncludeInHeaderPath
            <*> getFilePath "include-before-body-path" psIncludeBeforeBodyPath
            <*> getFilePath "include-after-body-path" psIncludeAfterBodyPath

mathJaxUrl :: String
mathJaxUrl = "https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_CHTML-full"

maybeReadFileUtf8 :: Maybe FilePath -> IO (Maybe String)
maybeReadFileUtf8 (Just path) = Just <$> readFileUtf8 path
maybeReadFileUtf8 Nothing = return Nothing

mkWriterOptions :: PandocSettings -> IO WriterOptions
mkWriterOptions PandocSettings{..} = do
    mbTemplate <- maybeReadFileUtf8 psTemplatePath
    mbIncludeInHeader <- maybeReadFileUtf8 psIncludeInHeaderPath
    mbIncludeBeforeBody <- maybeReadFileUtf8 psIncludeBeforeBodyPath
    mbIncludeAfterBody <- maybeReadFileUtf8 psIncludeAfterBodyPath

    -- TODO: Let's do this more elegantly! Looks a little like a fold...
    let psVars1 = case mbIncludeInHeader of
                    Nothing -> psVars
                    Just s -> ("header-includes", s) : psVars
        psVars2 = case mbIncludeBeforeBody of
                    Nothing -> psVars1
                    Just s -> ("include-before", s) : psVars1
        psVars3 = case mbIncludeAfterBody of
                    Nothing -> psVars2
                    Just s -> ("include-after", s) : psVars2

    let htmlMathMethod = if psMathJaxEnabled
                            then MathJax mathJaxUrl
                            else PlainMath

    return $ def
        { writerNumberSections = psNumberSections
        , writerReferenceDoc = psReferenceDoc
        , writerTemplate = mbTemplate
        , writerTableOfContents = psTableOfContents
        , writerHTMLMathMethod = htmlMathMethod
        , writerVariables = psVars3
        }

runner :: PandocSettings -> RunContext -> IO ()
runner
    ps@PandocSettings{..}
    (RunContext outputPath inputPaths _) = do

    putStrLn "PandocTool"
    putStrLn $ "  outputPath=" ++ outputPath
    putStrLn $ "  inputPaths=" ++ show inputPaths
    putStrLn $ "  psNumberSections=" ++ show psNumberSections
    putStrLn $ "  psReferenceDoc=" ++ show psReferenceDoc
    putStrLn $ "  psTableOfContents=" ++ show psTableOfContents
    putStrLn $ "  psTemplatePath=" ++ show psTemplatePath
    putStrLn $ "  psVars=" ++ show psVars

    md <- (intercalate "\n\n") <$> sequence (map readFileUtf8 inputPaths)

    let mdText = Text.pack md
        readerOpts = def
        exts = foldl'
                (flip enableExtension)
                (readerExtensions readerOpts)
                [Ext_backtick_code_blocks, Ext_yaml_metadata_block]
        readerOpts' = readerOpts { readerExtensions = exts }
        Right doc' = runPure $ readMarkdown readerOpts' mdText -- TODO: Irrefutable pattern
        doc = walk rewriteLinks doc'

    writerOpts <- mkWriterOptions ps

    -- TODO: Ugh. Let's make this less hacky. It works for now though.
    case (takeExtension outputPath) of
        ".docx" -> do
                        let Right docx = runPure $ writeDocx writerOpts doc -- TODO: Irrefutable pattern
                        BL.writeFile outputPath docx
        _ -> do
                        let Right html = runPure $ writeHtml5 writerOpts doc -- TODO: Irrefutable pattern
                            t = toEntities (Text.pack $ renderHtml html)
                        Text.writeFile outputPath t

-- TODO: This is very application-specific
-- TODO: Figure out how to allow this behaviour to be specified in app configuration
transformUrl :: String -> String
transformUrl url =
    let (path, ext) = splitExtension url
    in case ext of
        ".md" -> path
        _ -> url

-- TODO: This is very application-specific
-- TODO: Figure out how to allow this behaviour to be specified in app configuration
rewriteLinks :: Inline -> Inline
rewriteLinks (Link attr is (url, title)) = Link attr is (transformUrl url, title)
rewriteLinks i = i

pandocTool :: Tool
pandocTool = mkTool $ PandocSettings False [] Nothing False Nothing False Nothing Nothing Nothing

mkTool :: PandocSettings -> Tool
mkTool state = Tool "pandoc" updater' (runner state)
    where updater' ctx value = mkTool <$> updater state ctx value