stache-2.4.0: Text/Mustache/Render/Internal.hs
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module : Text.Mustache.Render.Internal
-- Copyright : © 2016–present Mark Karpov
-- License : BSD 3 clause
--
-- Maintainer : Mark Karpov <markkarpov92@gmail.com>
-- Stability : experimental
-- Portability : portable
--
-- Internal machinery of the rendering monad.
--
-- @since 2.4.0
module Text.Mustache.Render.Internal
( R,
runR,
IndentationPolicy (..),
txt,
incrementIndentBy,
addContext,
addPrefix,
resetPrefix,
addWarning,
getContext,
getPrefix,
lookupTemplate,
)
where
import Control.Monad.Reader (ReaderT, asks, local, runReaderT)
import Control.Monad.State.Strict (State, execState, modify')
import Data.Aeson (Value)
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NE
import Data.Map (Map)
import Data.Map qualified as M
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Lazy qualified as TL
import Data.Text.Lazy.Builder qualified as TLB
import Text.Megaparsec.Pos (Pos, mkPos, pos1, unPos)
import Text.Mustache.Type (Key, MustacheWarning, Node, PName)
-- | The 'R' monad supports printing with some helpful functionality to deal
-- with indentation.
newtype R a = R (ReaderT RC (State SC) a)
deriving (Functor, Applicative, Monad)
-- | The reader context of 'R'.
data RC = RC
{ -- | Indentation level, as the column index we need to start from after
-- a newline
rcIndent :: !Pos,
-- | The context stack
rcContext :: NonEmpty Value,
-- | The prefix accumulated by entering sections
rcPrefix :: Key,
-- | The template cache
rcTemplateCache :: Map PName [Node]
}
-- | The state context of 'R'.
data SC = SC
{ -- | Index of the next column to render
scColumn :: !Pos,
-- | Are we at the beginning of a newline?
scBeginningOfLine :: !Bool,
-- | Mustache warnings
scWarnings :: [MustacheWarning] -> [MustacheWarning],
-- | The output we have rendered so far (using Builder for efficiency)
scBuilder :: TLB.Builder
}
-- | The indentation policy to use when rendering text.
data IndentationPolicy
= -- | Indent every line
IndentEveryLine
| -- | Only indent the very first line, provided we are at the beginning
-- of the line
IndentOnlyFirstLine
-- | Run the 'R' monad.
runR ::
-- | Monad to run
R () ->
-- | The Context to use
Value ->
-- | The template cache
Map PName [Node] ->
([MustacheWarning], TL.Text)
runR (R m) v cache = (scWarnings s [], TLB.toLazyText (scBuilder s))
where
rc =
RC
{ rcIndent = pos1,
rcContext = v :| [],
rcPrefix = mempty,
rcTemplateCache = cache
}
sc =
SC
{ scColumn = pos1,
scBeginningOfLine = True,
scWarnings = id,
scBuilder = mempty
}
s = execState (runReaderT m rc) sc
-- | Output text, handling indentation appropriately. When we're at the
-- beginning of a line and the indentation level is non-zero, we first
-- output the appropriate amount of spaces.
txt ::
-- | Indentation policy to use
IndentationPolicy ->
-- | The text to output
Text ->
R ()
txt _ "" = R $ do
indent <- asks rcIndent
modify' $ \sc ->
if scBeginningOfLine sc && indent > pos1
then
let indentText = T.replicate (unPos indent - 1) " "
in sc
{ scColumn = mkPos (unPos indent),
scBeginningOfLine = False,
scBuilder = scBuilder sc <> TLB.fromText indentText
}
else sc
txt policy t = R $ do
indent <- asks rcIndent
modify' $ \sc ->
let processLine isFirstLine line sc' =
let shouldIndent = case policy of
IndentEveryLine ->
scBeginningOfLine sc'
&& not (T.null line)
&& indent > pos1
IndentOnlyFirstLine ->
isFirstLine
&& scBeginningOfLine sc'
&& not (T.null line)
&& indent > pos1
indentText =
if shouldIndent
then T.replicate (unPos indent - 1) " "
else T.empty
indentedLine = indentText <> line
newColumn =
if T.null indentedLine
then scColumn sc'
else mkPos (unPos (scColumn sc') + T.length indentedLine)
in sc'
{ scColumn = newColumn,
scBeginningOfLine = T.null indentedLine && scBeginningOfLine sc',
scBuilder = scBuilder sc' <> TLB.fromText indentedLine
}
go _ [] sc' = sc'
go isFirst [line] sc' = processLine isFirst line sc'
go isFirst (line : rest) sc' =
let sc'' = processLine isFirst line sc'
sc''' =
sc''
{ scColumn = pos1,
scBeginningOfLine = True,
scBuilder = scBuilder sc'' <> TLB.singleton '\n'
}
in go False rest sc'''
lines' = T.lines t
endsWithNewline = not (T.null t) && T.last t == '\n'
processedState = go True lines' sc
in if endsWithNewline
then
processedState
{ scColumn = pos1,
scBeginningOfLine = True,
scBuilder = scBuilder processedState <> TLB.singleton '\n'
}
else processedState
-- | Increment the indentation level by the given amount.
incrementIndentBy :: Pos -> R a -> R a
incrementIndentBy n (R m) =
R $
local (\rc -> rc {rcIndent = mkPos (unPos (rcIndent rc) + unPos n - 1)}) m
-- | Add a value to the context stack.
addContext :: Value -> R a -> R a
addContext v (R m) =
R $
local (\rc -> rc {rcContext = NE.cons v (rcContext rc)}) m
-- | Add to the key prefix.
addPrefix :: Key -> R a -> R a
addPrefix key (R m) =
R $
local (\rc -> rc {rcPrefix = rcPrefix rc <> key}) m
-- | Reset prefix.
resetPrefix :: R a -> R a
resetPrefix (R m) =
R $
local (\rc -> rc {rcPrefix = mempty}) m
-- | Add a warning to the warnings list.
addWarning :: MustacheWarning -> R ()
addWarning w = R $
modify' $
\sc -> sc {scWarnings = scWarnings sc . (w :)}
-- | Get the current context stack.
getContext :: R (NonEmpty Value)
getContext = R $ asks rcContext
-- | Get the current key prefix.
getPrefix :: R Key
getPrefix = R $ asks rcPrefix
-- | Lookup the template by its name.
lookupTemplate :: PName -> R (Maybe [Node])
lookupTemplate name = R $ asks $ \rc ->
M.lookup name (rcTemplateCache rc)