{-# LANGUAGE OverloadedStrings #-}

{-|
This module provides a standard way of building and generating blog posts.
Check out the Blog example
<https://github.com/elben/pencil/blob/master/examples/Blog/ here>. You can
also follow the [blogging tutorial
here](https://elbenshira.com/pencil/tutorials/03-blogging/).

To generate a blog for your website, first create a @blog/@ directory in
your web page source directory.

Then, name your blog posts in this format:

> yyyy-mm-dd-title-of-blog-post.markdown

Where @yyyy-mm-dd@ should be something like @2019-12-30@. This isn't used for
anything other than to keep each post ordered in the directory, for your ease
of viewing.

Each post is expected to have a preamble that has at least @postTitle@ and
@date@ defined. The date set in the preamble is used as the sort order of the
blog posts. The other variables are optional.

> <!--PREAMBLE
> postTitle: "The Meaning of Life"
> date: 2010-01-30
> draft: true
> tags:
>   - philosophy
> -->

You can mark a post as a draft via the @draft@ variable, so that it won't be
loaded when you call 'loadPosts'. You can also set the post's tags using,
as seen above in @tags@. Then, use 'loadPosts' to load the entire @blog/@
directory.

In the example below, @layout.html@ defines the outer HTML structure (with
global components like navigation), and @blog-post.html@ is a generic blog
post container that renders @${postTitle}@ as a header, @${date}@, and
@${body}@ for the post body.

@
layout <- 'load' "layout.html"
postLayout <- 'load' "blog-post.html"
posts <- 'loadPosts' "blog/"
render (fmap (layout <|| postLayout <|) posts)
@
-}
module Pencil.Blog
  ( loadPosts
  , postUrl
  , injectTitle

  -- * Tags
  -- | You can add tags to blog posts.
  , Tag
  , buildTagPages
  , buildTagPagesWith
  , injectTags
  ) where

import Pencil.Config
import Pencil.Env
import Pencil.App
import Pencil.Content
import Control.Monad (foldM)
import Control.Monad.Reader (asks)
import qualified Data.HashMap.Strict as H
import qualified Data.List as L
import qualified Data.Text as T
import qualified System.FilePath as FP

-- | Loads the given directory as a series of blog posts, sorted by the @date@
-- preamble environment variable. Posts with @draft: true@ are filtered out.
--
-- @
-- posts <- loadPosts "blog/"
-- @
loadPosts :: FilePath -> PencilApp [Page]
loadPosts fp = do
  posts <- loadDir False fp
  return $
    (filterByVar True "draft" (VBool True /=) . sortByVar "date" dateOrdering)
    (fmap (rename postUrl) posts)

-- | Rewrites file path for blog posts.
--
-- > postUrl "/blog/2011-01-01-post-title.html"
-- > -- "/blog/1-post-title.html/"
--
postUrl :: FilePath -> FilePath
postUrl fp = FP.replaceFileName fp (drop 11 (FP.takeBaseName fp)) ++ "/"

-- | Given that the current @Page@ has a @postTitle@ in the environment, inject
-- the post title into the @title@ environment variable, prefixed with the given
-- title prefix.
--
-- This is useful for generating the @\<title\>${title}\</title\>@ tags in your
-- container layout.
--
-- For example, if the page's preamble has @postTitle: "The Meaning of Life"@,
-- then the snippet below will insert a @title@ variable with the value @"The
-- Meaning of Life - My Awesome Website"@:
--
-- @
-- injectTitle "My Awesome Website" post
-- @
--
injectTitle :: T.Text
            -- ^ Title prefix
            -> Page
            -> Page
injectTitle titlePrefix page =
  let title = case H.lookup "postTitle" (getPageEnv page) of
                       Just (VText t) -> T.append (T.append t " - ") titlePrefix
                       _ -> titlePrefix
      env' = insertText "title" title (getPageEnv page)
  in setPageEnv env' page

-- | Like, you know, a hashtag. Wraps a text.
type Tag = T.Text

-- | Finds all the tags from the given pages, and generates a page for each tag
-- found. Each tag page has a variable "posts" containing all pages that have
-- the tag.
--
-- Helper of 'buildTagPagesWith' defaulting to the variable name @posts@, and
-- the tag index page file path @blog\/tags\/my-tag-name\/@.
--
-- @
-- tagPages <- buildTagPages pages
-- @
--
buildTagPages :: FilePath
              -> [Page]
              -> PencilApp (H.HashMap Tag Page)

buildTagPages tagPageFp =
  buildTagPagesWith
    tagPageFp
    "posts"
    (\tag _ -> "blog/tags/" ++ T.unpack tag ++ "/")

-- | Build the tag index pages.
--
-- Given blog post @Page@s with @tags@ variables in its PREAMBLE, builds @Page@s that
-- contain in its environment the list of @Page@s that were tagged with that
-- particular tag. Returns a map of tag of the tag index page.
--
-- @
-- tagPages <- buildTagPagesWith
--               "tag-list.html"
--               "posts"
--               (\\tag _ -> "blog\/tags\/" ++ 'Data.Text.unpack' tag ++ "\/")
--               posts
-- @
buildTagPagesWith :: FilePath
                  -- ^ Partial to load for the Tag index pages
                  -> T.Text
                  -- ^ Variable name inserted into Tag index pages for the list of
                  -- Pages tagged with the specified tag
                  -> (Tag -> FilePath -> FilePath)
                  -- ^ Function to generate the URL of the tag pages
                  -> [Page]
                  -> PencilApp (H.HashMap Tag Page)
buildTagPagesWith tagPageFp pagesVar fpf pages = do
  env <- asks getEnv

  let tagMap = groupByElements "tags" pages
  -- Build a mapping of tag to the tag list Page

  foldM
    (\acc (tag, taggedPosts) -> do
      tagPage <- rename (fpf tag) <$> load tagPageFp
      -- Generate the URL that this tag page will use.
      let url = T.pack $ "/" ++ getFilePath tagPage
      tagEnv <- (insertPages pagesVar taggedPosts . insertText "tag" tag . insertText "this.url" url . merge (getPageEnv tagPage)) env
      return $ H.insert tag (setPageEnv tagEnv tagPage) acc
    )
    H.empty
    (H.toList tagMap)

-- | Injects the tag map (usually generated by 'buildTagPages' or
-- 'buildTagPagesWith') into the page's environment as the variable @tags@,
-- which is an @VEnvList@.
injectTags :: H.HashMap Tag Page -> Page -> Page
injectTags tagMap page =
  -- Build up an env list of tag to that tag page's env. This is so that we can
  -- have access to the URL of the tag index pages.
  let envs =
        case H.lookup "tags" (getPageEnv page) of
          Just (VArray tags) ->
              L.foldl'
                (\acc envData ->
                  case envData of
                    VText tag ->
                      case H.lookup tag tagMap of
                        Just tagIndexPage -> getPageEnv tagIndexPage : acc
                        _ -> acc
                    _ -> acc)
                [] tags
          _ -> []

      -- Overwrite the VArray "tags" variable in the post Page with VEnvList of the
      -- loaded Tag index pages. This is so that when we render the blog posts, we
      -- have access to the URL of the Tag index.
      env' = if null envs
               then getPageEnv page
               else insert "tags" (VEnvList envs) (getPageEnv page)
  in setPageEnv env' page