packages feed

ginger2-2.0.0.0: cli/Main.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

{-# OPTIONS_GHC -Werror #-}

module Main where

import Language.Ginger
import Language.Ginger.Interpret.Builtins (textBuiltin)
import Language.Ginger.Value

import qualified CMark
import CMark (commonmarkToHtml)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
import Options.Applicative
import System.Directory (getCurrentDirectory)
import System.FilePath (takeDirectory, takeFileName, takeExtension, (</>) )
import System.IO (hPutStrLn, stderr)

import qualified Data.Yaml as YAML

data EncoderChoice
  = HtmlEncoder
  | TextEncoder
  | AutoEncoder
  deriving (Show, Eq, Ord, Enum, Bounded)

data ProgramOptions =
  ProgramOptions
    { poDataFiles :: [FilePath]
    , poSourceFile :: Maybe FilePath
    , poOutputFile :: Maybe FilePath
    , poTrimBlocks :: BlockTrimming
    , poStripBlocks :: BlockStripping
    , poEncoder :: EncoderChoice
    , poDialect :: JinjaDialect
    }
    deriving (Show, Eq)

defProgramOptions :: ProgramOptions
defProgramOptions =
  ProgramOptions
    { poDataFiles = []
    , poSourceFile = Nothing
    , poOutputFile = Nothing
    , poTrimBlocks = pstateTrimBlocks defPOptions
    , poStripBlocks = pstateStripBlocks defPOptions
    , poEncoder = HtmlEncoder
    , poDialect = DialectGinger2
    }

programOptions :: Parser ProgramOptions
programOptions =
  ProgramOptions
    <$> many
          (
            argument str
              ( metavar "DATAFILE"
              <> help "JSON or YAML data file"
              )
          <|>
            strOption
              ( metavar "DATAFILE"
              <> short 'd'
              <> long "data-file"
              <> help "JSON or YAML data file"
              )
          )
    <*> option (Just <$> str)
          ( long "template"
          <> short 't'
          <> metavar "TEMPLATE"
          <> help "Template file (STDIN if not provided)"
          <> value Nothing
          )
    <*> option (Just <$> str)
          ( long "output"
          <> short 'o'
          <> metavar "OUTFILE"
          <> help "Output file (STDOUT if not provided)"
          <> value Nothing
          )
    <*> ( flag' TrimBlocks
            ( long "trim-blocks"
            <> help "Enable block trimming"
            )
          <|>
          flag' NoTrimBlocks
            ( long "no-trim-blocks"
            <> help "Disable block trimming"
            )
          <|> pure (pstateTrimBlocks defPOptions)
        )
    <*> ( flag' StripBlocks
            ( long "strip-blocks"
            <> help "Enable block stripping"
            )
          <|>
          flag' NoStripBlocks
            ( long "no-strip-blocks"
            <> help "Disable block stripping"
            )
          <|> pure (pstateStripBlocks defPOptions)
        )
    <*> option encoderReader
          ( long "encoder"
          <> metavar "ENCODER"
          <> help
              ( "Output encoding ('html', 'text', or 'auto'). " ++
                "'auto' will guess encoding from output file extension, then " ++
                "template file extension, then default to 'html'")
          <> value AutoEncoder
          )
    <*> option dialectReader
          ( long "dialect"
          <> metavar "DIALECT"
          <> help
              ( "Jinja dialect. Valid options: " ++
                "'jinja' (compatibility mode), " ++
                "'ginger' (ginger2-specific extensions, default)"
              )
          <> value DialectGinger2
          )

encoderReader :: ReadM EncoderChoice
encoderReader = eitherReader $ \case
  "html" -> Right HtmlEncoder
  "text" -> Right TextEncoder
  "auto" -> Right AutoEncoder
  s -> Left $ "Invalid encoder: " ++ show s
        
dialectReader :: ReadM JinjaDialect
dialectReader = eitherReader $ \case
  "ginger" -> Right DialectGinger2
  "ginger2" -> Right DialectGinger2

  "jinja" -> Right DialectJinja2
  "jinja2" -> Right DialectJinja2
  "compat" -> Right DialectJinja2
  s -> Left $ "Invalid dialect: " ++ show s
        


main :: IO ()
main = do
  po <- execParser $
          info (programOptions <**> helper)
            ( fullDesc
            <> header "ginger - command-line jinja2 template interpreter"
            )
  runWithOptions po

fileOrStdinLoader :: FilePath -> TemplateLoader IO
fileOrStdinLoader baseDir templateName =
  case templateName of
    "" -> Just <$> Text.getContents
    n -> Just <$> Text.readFile (baseDir </> Text.unpack n)

textEncoder :: Encoder IO
textEncoder txt = do
  pure $ Encoded txt

loadDataFile :: FilePath -> IO (Map Identifier (Value IO))
loadDataFile path = do
  YAML.decodeFileThrow path

runWithOptions :: ProgramOptions -> IO ()
runWithOptions po = do
  (baseDir, templateName) <- case poSourceFile po of
    Nothing -> (,) <$> getCurrentDirectory <*> pure ""
    Just path -> pure (takeDirectory path, Text.pack $ takeFileName path)
  vars <- mconcat <$> mapM loadDataFile (poDataFiles po)
  let encoder = case poEncoder po of
        HtmlEncoder -> htmlEncoder
        TextEncoder -> textEncoder
        AutoEncoder ->
          let outputExt = maybe "" takeExtension $ poOutputFile po
              ext = case outputExt of
                      "" -> takeExtension (Text.unpack templateName)
                      _ -> outputExt
          in case ext of
            ".txt" -> textEncoder
            ".text" -> textEncoder
            _ -> htmlEncoder
  ginger
    (fileOrStdinLoader baseDir)
    defPOptions
      { pstateTrimBlocks = poTrimBlocks po
      , pstateStripBlocks = poStripBlocks po
      }
    (poDialect po)
    encoder
    templateName
    (vars <> extensions) >>= printResultTo (poOutputFile po)

printResult :: Either RuntimeError Encoded -> IO ()
printResult = printResultTo Nothing

printResultTo :: Maybe FilePath -> Either RuntimeError Encoded -> IO ()
printResultTo _ (Left err) =
  hPutStrLn stderr $ prettyRuntimeError err
printResultTo Nothing (Right output) =
  Text.putStrLn $ encoded output
printResultTo (Just outputPath) (Right output) =
  Text.writeFile outputPath $ encoded output

demo :: IO ()
demo = runWithOptions $ defProgramOptions
  { poSourceFile = Just "./test.html"
  , poEncoder = TextEncoder
  }

extensions :: forall m. Monad m => Map Identifier (Value m)
extensions = Map.fromList
  [ ("markdown"
    , textBuiltin
        "extensions:markdown"
        (Just ProcedureDoc
          { procedureDocName = "markdown"
          , procedureDocArgs =
              [ ArgumentDoc
                  "value"
                  (Just $ TypeDocSingle "string")
                  Nothing
                  "Markdown source (CommonMark)"
              ]
          , procedureDocReturnType = (Just $ TypeDocSingle "encoded")
          , procedureDocDescription = "Convert CommonMark to HTML"
          }
        )
        (EncodedV @m . Encoded . commonmarkToHtml [CMark.optSafe])
    )
  ]