packages feed

hakyllbars-1.0.0.0: src/Hakyllbars/Compiler.hs

module Hakyllbars.Compiler where

import Control.Applicative (liftA3)
import Control.Monad.State.Strict
import Data.Bifunctor
import Data.List.NonEmpty as NonEmpty
import Hakyll.Core.Compiler.Internal
import Hakyllbars.Ast
import Hakyllbars.Common
import Hakyllbars.Context hiding (field)
import Hakyllbars.Source.Parser (parse)

-- | Takes an item and compiles a template from it.
compileTemplateItem :: Item String -> Compiler Template
compileTemplateItem item = do
  let filePath = toFilePath $ itemIdentifier item
  either (fail . show) return $ parse filePath (itemBody item)

loadTemplate :: Identifier -> TemplateRunner a Template
loadTemplate = lift . fmap itemBody . load

applyTemplate :: Identifier -> TemplateRunner String ()
applyTemplate =
  loadTemplate
    >=> reduceTemplate
    >=> lift . makeItem
    >=> tplPushItem

applyAsTemplate :: TemplateRunner String ()
applyAsTemplate =
  tplModifyItem do
    lift . compileTemplateItem
      >=> reduceTemplate
      >=> lift . makeItem

reduceTemplate :: Template -> TemplateRunner String String
reduceTemplate (Template bs src) =
  tplWithCall ("template " ++ src) (reduceBlocks bs)

reduceBlocks :: [Block] -> TemplateRunner String String
reduceBlocks = stringify . intoValue <=< applyBlocks

applyBlocks :: [Block] -> TemplateRunner String [ContextValue String]
applyBlocks = mapM applyBlock

applyBlock :: Block -> TemplateRunner String (ContextValue String)
applyBlock = tplWithPos getBlockPos \case
  TextBlock t _ -> return $ intoValue t
  ExpressionBlock e _ -> eval e
  CommentBlock {} -> return EmptyValue
  ChromeBlock e bs pos ->
    fmap intoValue do
      bs' <- reduceBlocks bs
      eval e >>= \case
        FunctionValue f -> f (intoValue bs')
        x -> tplFail $ "invalid chrome function " ++ show x ++ " near " ++ show pos
  AltBlock (ApplyBlock e bs _ :| alts) maybeDefault _ ->
    intoValue <$> applyAltBlock e bs alts maybeDefault

applyAltBlock ::
  Expression ->
  [Block] ->
  [ApplyBlock] ->
  Maybe DefaultBlock ->
  TemplateRunner String [ContextValue String]
applyAltBlock guard' bs alts maybeDefault =
  eval guard' >>= \case
    FunctionValue f -> pure <$> f (intoValue bs)
    x ->
      isTruthy x >>= \case
        True -> applyBlocks bs
        False -> case alts of
          ApplyBlock guard'' bs' _ : alts' -> applyAltBlock guard'' bs' alts' maybeDefault
          [] -> case maybeDefault of
            Just (DefaultBlock bs' _) -> applyBlocks bs'
            Nothing -> return []

eval :: Expression -> TemplateRunner a (ContextValue a)
eval = tplWithPos getExpressionPos \case
  NameExpression name pos -> do
    (context, item, trace) <-
      liftA3 (,,) tplContext tplItem tplTrace `catchError` \e -> do
        tplFail $ "Caught error in template: " ++ show e ++ " near " ++ show pos
    s <- get
    (x, s') <-
      lift $
        runStateT (unContext context name) s `compilerCatch` \case
          CompilationFailure ne ->
            compilerThrow (NonEmpty.toList ne)
          CompilationNoResult ss ->
            -- TODO figure out how to get state changes to persist if value comes back empty
            return (UndefinedValue name item (show pos : trace) ss, s)
    put s'
    return x
  StringExpression s _ -> return $ intoValue s
  IntExpression n _ -> return $ intoValue n
  DoubleExpression x _ -> return $ intoValue x
  BoolExpression b _ -> return $ intoValue b
  ApplyExpression f x _ -> apply f x
  AccessExpression target field pos ->
    eval target >>= \case
      ContextValue target' -> do
        name <-
          eval field >>= \case
            StringValue x -> return x
            x -> tplFail $ "invalid field " ++ show x ++ " near " ++ show (getExpressionPos field)
        s <- get
        (x, s') <-
          lift $
            runStateT (unContext target' name) s `compilerCatch` \case
              CompilationFailure errors ->
                compilerThrow (NonEmpty.toList errors)
              CompilationNoResult _ ->
                -- TODO figure out how to get state changes to persist if value comes back empty
                return (EmptyValue, s)
        put s'
        return x
      EmptyValue -> return EmptyValue
      UndefinedValue {} -> return EmptyValue
      x -> tplFail $ "invalid context " ++ show x ++ " near " ++ show pos
  FilterExpression x f _ -> apply f x
  ContextExpression pairs _ -> do
    pairs' <- mapM (sequence . second eval) pairs
    return $ ContextValue (intoContext pairs')
  ListExpression xs _ -> intoValue <$> mapM eval xs
  where
    apply f x =
      eval f >>= \case
        FunctionValue f' -> f' (ThunkValue $ eval x)
        x' -> tplFail $ "invalid function " ++ show x' ++ " in " ++ show (getExpressionPos f)

stringify :: ContextValue String -> TemplateRunner String String
stringify = \case
  EmptyValue -> return ""
  UndefinedValue name item trace errors ->
    tplFail $
      "can't stringify undefined value "
        ++ show name
        ++ "\nin item context for "
        ++ itemFilePath item
        ++ "\ntrace=[\n\t"
        ++ intercalate ",\n\t" trace
        ++ "\n],\nsuppressed=[\n\t"
        ++ intercalate ",\n\t" errors
        ++ "\n]\n"
  ContextValue {} -> tplFail "can't stringify context"
  ListValue xs -> mconcat <$> mapM stringify xs
  BoolValue b -> return $ show b
  StringValue s -> return s
  DoubleValue x -> return $ show x
  IntValue n -> return $ show n
  FunctionValue {} -> tplFail "can't stringify function"
  BlockValue block -> case block of
    TextBlock t _ -> return t
    CommentBlock {} -> return ""
    ExpressionBlock e _ -> stringify =<< eval e
    _ -> stringify =<< applyBlock block
  ItemValue item -> return $ itemBody item
  ThunkValue fx -> stringify =<< force =<< fx
  PairValue (_, x) -> stringify x

isTruthy :: ContextValue a -> TemplateRunner a Bool
isTruthy = \case
  EmptyValue -> return False
  UndefinedValue {} -> return False
  ContextValue {} -> return True
  ListValue xs -> return $ not (null xs)
  BoolValue x -> return x
  StringValue x -> return $ not (null x)
  DoubleValue x -> return $ x /= 0
  IntValue x -> return $ x /= 0
  FunctionValue {} -> return True
  BlockValue {} -> return True
  ItemValue _ -> return True
  ThunkValue fx -> isTruthy =<< force =<< fx
  PairValue (_, x) -> isTruthy x

force :: ContextValue a -> TemplateRunner a (ContextValue a)
force = \case
  ThunkValue fx -> force =<< fx
  x -> return x