packages feed

queue-sheet-0.7.0.2: src/QueueSheet/Template.hs

------------------------------------------------------------------------------
-- |
-- Module      : QueueSheet.Template
-- Description : queue sheet template functions
-- Copyright   : Copyright (c) 2020-2022 Travis Cardwell
-- License     : MIT
------------------------------------------------------------------------------

{-# LANGUAGE CPP #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

module QueueSheet.Template
  ( -- * API
    loadTemplate
  , renderTemplate
  ) where

-- https://hackage.haskell.org/package/base
import Data.Bifunctor (first)
#if !MIN_VERSION_base (4,11,0)
import Data.Monoid ((<>))
#endif
import qualified System.IO as IO

-- https://hackage.haskell.org/package/ginger
import qualified Text.Ginger as Ginger
import Text.Ginger ((~>))

-- https://hackage.haskell.org/package/text
import Data.Text (Text)
import qualified Data.Text.IO as TIO

-- https://hackage.haskell.org/package/transformers
import Control.Monad.Trans.Writer (Writer)

-- (queue-sheet)
import QueueSheet.Types
  ( Date, Item, Name
  , Queue
      ( Queue, queueDate, queueItems, queueName, queueSection, queueTags
      , queueUrl
      )
  , QueueSheet(QueueSheet, qsQueues, qsSections), Section, Tag(Tag), Url
  )

------------------------------------------------------------------------------
-- $QueueCtx

-- | Queue context
data QueueCtx
  = QueueCtx
    { name      :: !Name
    , url       :: !(Maybe Url)
    , date      :: !(Maybe Date)
    , tags      :: ![Tag]
    , prevItem  :: !(Maybe Item)
    , nextItems :: ![Item]
    }

instance Ginger.ToGVal m QueueCtx where
  toGVal QueueCtx{..} = Ginger.dict $
    [ "name"       ~> name
    , "url"        ~> url
    , "date"       ~> date
    , "prev_item"  ~> prevItem
    , "next_items" ~> nextItems
    ] ++ [("tag_" <> tag) ~> True | Tag tag <- tags]

-- | Construct a queue context
queueCtx :: Queue -> QueueCtx
queueCtx Queue{..} = QueueCtx
    { name      = queueName
    , url       = queueUrl
    , date      = queueDate
    , tags      = queueTags
    , prevItem  = either Just (const Nothing) =<< queueItems
    , nextItems = maybe [] (either (const []) id) queueItems
    }

------------------------------------------------------------------------------
-- $SectionCtx

-- | Section context
newtype SectionCtx = SectionCtx (Section, [QueueCtx])

instance Ginger.ToGVal m SectionCtx where
  toGVal (SectionCtx (section, queues)) = Ginger.dict
    [ "name"   ~> section
    , "queues" ~> queues
    ]

-- | Check if a section context has any queues
sectionCtxHasQueues :: SectionCtx -> Bool
sectionCtxHasQueues (SectionCtx (_, [])) = False
sectionCtxHasQueues _                    = True

------------------------------------------------------------------------------
-- $Context

-- | Template context
newtype Context = Context [SectionCtx]
  deriving newtype (Ginger.ToGVal m)

-- | Template context constructor
context :: [Section] -> [Queue] -> Context
context sections queues = Context $ filter sectionCtxHasQueues
    [ SectionCtx
        ( section
        , [ queueCtx queue
          | queue <- queues, queueSection queue == section
          ]
        )
    | section <- sections
    ]

-- | Create a Ginger context from a template context
gingerContext
  :: Context
  -> Ginger.GingerContext Ginger.SourcePos (Writer Text) Text
gingerContext ctx = Ginger.makeContextText $ \case
    "sections" -> Ginger.toGVal ctx
    _          -> Ginger.toGVal (Nothing :: Maybe Text)

------------------------------------------------------------------------------
-- $API

-- | Load a Ginger template
--
-- @since 0.3.0.0
loadTemplate
  :: FilePath
  -> IO (Either String (Ginger.Template Ginger.SourcePos))
loadTemplate path = first formatError <$> Ginger.parseGingerFile' options path
  where
    options :: Ginger.ParserOptions IO
    options = Ginger.ParserOptions
      { poIncludeResolver     = fmap Just . IO.readFile
      , poSourceName          = Nothing
      , poKeepTrailingNewline = False
      , poLStripBlocks        = False
      , poTrimBlocks          = False
      , poDelimiters          = Ginger.Delimiters
          { delimOpenInterpolation  = "<<"
          , delimCloseInterpolation = ">>"
          , delimOpenTag            = "<!"
          , delimCloseTag           = "!>"
          , delimOpenComment        = "<#"
          , delimCloseComment       = "#>"
          }
      }

    formatError :: Ginger.ParserError -> String
    formatError err = concat
      [ "error loading template: "
      , maybe path show $ Ginger.peSourcePosition err
      , ": "
      , Ginger.peErrorMessage err
      ]

-- | Render a template using the given context
--
-- @since 0.3.0.0
renderTemplate
  :: FilePath
  -> Ginger.Template Ginger.SourcePos
  -> QueueSheet
  -> IO ()
renderTemplate path template QueueSheet{..} =
    let ctx = gingerContext $ context qsSections qsQueues
    in  TIO.writeFile path $ Ginger.runGinger ctx template