packages feed

hakyllbars-1.0.0.0: src/Hakyllbars/Field.hs

{-# LANGUAGE AllowAmbiguousTypes #-}

module Hakyllbars.Field
  ( module Hakyllbars.Field.Date,
    module Hakyllbars.Field.Git,
    module Hakyllbars.Field.Html,
    defaultFields,
    emptyString,
    defaultKeys,
    includeField,
    layoutField,
    ifField,
    forField,
    withField,
    forEachField,
    defaultField,
    linkedTitleField,
    metadataField,
    siteUrlField,
    urlField,
    absUrlField,
    getUrlField,
    getAbsUrlField,
    titleFromFileField,
    teaserField,
    metadataPriorityField,
    namedMetadataField,
    putField,
    addField,
    putBlockField,
    addBlockField,
  )
where

import Control.Monad.Except
import Data.Aeson.Key qualified as Key
import Data.Aeson.KeyMap qualified as KeyMap
import Hakyllbars.Ast
import Hakyllbars.Common
import Hakyllbars.Compiler
import Hakyllbars.Context
import Hakyllbars.Field.Date (DateConfig, dateFields, defaultDateConfigWith)
import Hakyllbars.Field.Git (gitFields)
import Hakyllbars.Field.Html (escapeHtmlField, escapeHtmlUriField)
import Hakyllbars.Util (stripSuffix)
import System.FilePath

defaultFields :: String -> String -> Context String
defaultFields host siteRoot =
  mconcat
    [ bodyField "body",
      constField "host" host,
      constField "siteRoot" siteRoot,
      pathField "path",
      siteUrlField "siteUrl" "host" "siteRoot",
      urlField "url" "siteRoot",
      absUrlField "absUrl" "host" "url",
      getUrlField "getUrl" "siteRoot",
      getAbsUrlField "getAbsUrl" "host" "getUrl",
      linkedTitleField "linkedTitle" "title" "url",
      escapeHtmlField,
      escapeHtmlUriField,
      putField "put",
      addField "add",
      putBlockField "putBlock",
      addBlockField "addBlock",
      ifField "if",
      forField "for",
      defaultField "default",
      withField "with",
      includeField "include" Nothing Nothing,
      includeField "partial" (Just "_partials") (Just "html"),
      layoutField "applyLayout" "_layouts" (Just "html"),
      metadataPriorityField "updated" ["updated", "published", "created"],
      metadataPriorityField "published" ["published", "created"],
      metadataField,
      titleFromFileField "title",
      constField "description" ("" :: String)
    ]

emptyString :: ContextValue a
emptyString = intoValue ("" :: String)

defaultKeys :: [String] -> Context a
defaultKeys keys = intoContext $ (,"" :: String) <$> keys

withField :: String -> Context String
withField key = functionField2 key f
  where
    f (context :: Context String) (blocks :: [Block]) =
      tplWithContext context do
        reduceBlocks blocks

includeField :: String -> Maybe FilePath -> Maybe FilePath -> Context String
includeField key basePath extension = functionField key f
  where
    f (filePath :: String) = do
      basePath' <- maybe (itemFilePath <$> tplItem) return basePath
      let filePath' = basePath' </> filePath
      let filePath'' = maybe filePath' (filePath' <.>) extension
      context <- tplContext
      applyTemplate (fromFilePath filePath'')
      itemValue context <$> tplPopItem

layoutField :: String -> FilePath -> Maybe FilePath -> Context String
layoutField key basePath extension = functionField2 key f
  where
    f (filePath :: FilePath) (content :: String) = do
      let filePath' = basePath </> filePath
      let filePath'' = maybe filePath' (filePath' <.>) extension
      let layoutId = fromFilePath filePath''
      (Template bs _) <- loadTemplate layoutId
      item <- itemSetBody content <$> tplItem
      tplWithItem item do
        reduceBlocks bs

ifField :: forall a. String -> Context a
ifField key = functionField key isTruthy

forField :: String -> Context String
forField key = functionField2 key applyForLoop

applyForLoop :: ContextValue String -> [Block] -> TemplateRunner String (Maybe String)
applyForLoop items blocks =
  getAsItems items
    `catchError` (\_ -> getAsStrings items)
    `catchError` (\_ -> return (mempty, []))
    >>= uncurry go
  where
    go context items'
      | null items' = return Nothing
      | otherwise = tplWithContext context do
          Just . mconcat <$> forM items' \item ->
            tplWithItem item do
              reduceBlocks blocks

getAsItems :: ContextValue String -> TemplateRunner String (Context String, [Item String])
getAsItems = fromValue

getAsStrings :: ContextValue String -> TemplateRunner String (Context String, [Item String])
getAsStrings x = do
  bodies <- fromValue x :: TemplateRunner String [String]
  items <- forM bodies \body -> itemSetBody body <$> tplItem
  return (bodyField "item", items)

forEachField :: String -> Context String
forEachField key = functionField3 key f
  where
    f (forEachKey :: ContextValue String) (forEachItems :: ContextValue String) (blocks :: [Block]) = do
      keyId <- getKey forEachKey
      keyItemPairs <- fromValue forEachItems :: TemplateRunner String [(ContextValue String, ContextValue String)]
      keyItemPairs `forM` \(key', items) ->
        tplWithContext (constField keyId key') do
          applyForLoop items blocks
    getKey block = case block of
      UndefinedValue k _ _ _ -> return k -- allow identifier as key
      StringValue k -> return k
      _ -> tplFail "forEach: key must be a string or identifier"

defaultField :: forall a. String -> Context a
defaultField key = functionField2 key f
  where
    f (default' :: ContextValue a) (arg :: ContextValue a) =
      isTruthy arg <&> \case
        True -> arg
        False -> default'

linkedTitleField :: String -> String -> String -> Context String
linkedTitleField key titleKey urlKey = constField key f
  where
    f :: FunctionValue String String String
    f filePath = do
      tplWithItem (Item (fromFilePath filePath) "") do
        makeLink <$> getField titleKey <*> getField urlKey
      where
        getField key' = do
          context <- tplContext
          fromValue =<< unContext context key'
        makeLink title url
          | ".html" `isSuffixOf` filePath = "<a href=\"" ++ escapeHtml url ++ "\" title=\"" ++ escapeHtml title ++ "\">" ++ escapeHtml title ++ "</a>"
          | ".md" `isSuffixOf` filePath = "[" ++ title ++ "](" ++ url ++ " \"" ++ title ++ "\")"
          | otherwise = title ++ " <" ++ url ++ ">"

metadataField :: forall a. Context a
metadataField = Context f
  where
    f key = lift . getMetadataField key =<< tplItem

getMetadataField :: String -> Item a -> Compiler (ContextValue a)
getMetadataField key item = do
  m <- getMetadata (itemIdentifier item)
  maybe
    (noResult $ "tried metadata key " ++ show key)
    (return . intoValue)
    (KeyMap.lookup (Key.fromString key) m)

bodyField :: String -> Context String
bodyField key = field key $ return . itemBody

siteUrlField :: String -> String -> String -> Context a
siteUrlField key hostKey siteRootKey = field key f
  where
    f _ = do
      context <- tplContext
      host <- fromValue =<< unContext context hostKey
      siteRoot <- fromValue =<< unContext context siteRootKey
      return (host ++ siteRoot :: String)

urlField :: String -> String -> Context a
urlField key siteRootKey = field key f
  where
    f = getUri key siteRootKey . itemIdentifier

getUrlField :: String -> String -> Context a
getUrlField key siteRootKey = functionField key f
  where
    f = getUri key siteRootKey . fromFilePath

getUri :: String -> String -> Identifier -> TemplateRunner a String
getUri key siteRootKey id' = do
  siteRoot <-
    tplContext
      >>= flip unContext siteRootKey
      >>= fromValue
  maybeRoute <- lift $ getRoute id'
  definitelyRoute <-
    maybe
      (fail $ "no route by " ++ show key ++ " found for item " ++ show id')
      (return . ("/" ++))
      maybeRoute
  let uri = stripSuffix "index.html" definitelyRoute
  return if null uri then siteRoot else siteRoot ++ uri

absUrlField :: String -> String -> String -> Context a
absUrlField key hostKey urlKey = field key f
  where
    f _ = do
      context <- tplContext
      host <- fromValue =<< unContext context hostKey
      url <- fromValue =<< unContext context urlKey
      return (host ++ url :: String)

getAbsUrlField :: forall a. String -> String -> String -> Context a
getAbsUrlField key hostKey getUrlKey = functionField key f
  where
    f (filePath :: FilePath) = do
      context <- tplContext
      host <- fromValue =<< unContext context hostKey
      getUrl <- fromValue =<< unContext context getUrlKey
      url <- getUrl (intoValue filePath :: ContextValue a)
      return (host ++ url :: String)

pathField :: String -> Context a
pathField key = field key $ return . toFilePath . itemIdentifier

titleFromFileField :: String -> Context a
titleFromFileField = bindField titleFromPath . pathField
  where
    titleFromPath = return . takeBaseName

teaserField :: String -> Snapshot -> Context String
teaserField key snapshot = field key f
  where
    f item = lift do
      body <- loadSnapshotBody (itemIdentifier item) snapshot
      case takeTeaser body of
        Just teaser -> return teaser
        Nothing -> fail $ "item " ++ itemFilePath item ++ " has no teaser"
    takeTeaser = go ""
      where
        go acc xss@(x : xs)
          | "<!--more-->" `isPrefixOf` xss = Just (reverse acc)
          | otherwise = go (x : acc) xs
        go _ [] = Nothing

metadataPriorityField :: String -> [String] -> Context a
metadataPriorityField key priorityKeys = field key f
  where
    f item =
      lift $
        foldl
          (<|>)
          (noResult $ "Metadata priority key " ++ show key ++ " from set " ++ show priorityKeys)
          (flip getMetadataField item <$> priorityKeys)

namedMetadataField :: String -> Context String
namedMetadataField key = field key $ lift . getMetadataField key

putField :: String -> Context a
putField key = functionField key tplPut

addField :: forall a. String -> Context a
addField key = functionField2 key f
  where
    f (name :: String) (value :: ContextValue a) = do
      current <- tplGet name `catchError` \_ -> return []
      tplPut $ constField name (value : current)

putBlockField :: String -> Context a
putBlockField key = functionField2 key f
  where
    f (name :: String) (blocks :: [Block]) = do
      tplPut $ constField name blocks

addBlockField :: String -> Context a
addBlockField key = functionField2 key f
  where
    f (name :: String) (blocks :: [Block]) = do
      current <- tplGet name `catchError` \_ -> return []
      tplPut $ constField name (current ++ blocks)