pencil-0.1.1: src/Pencil/Blog.hs
{-# LANGUAGE OverloadedStrings #-}
module Pencil.Blog
(
-- * Getting started
--
-- $gettingstarted
--
loadBlogPosts
, blogPostUrl
, injectTitle
, buildTagPagesWith
, buildTagPages
, injectTagsEnv
) where
import Pencil
import Pencil.Internal.Env
import Control.Monad (liftM, 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
-- $gettingstarted
--
-- 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>.
--
-- 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
--
-- The files in that directory are expected to have preambles that have at
-- least @postTitle@ and @date@ defined. The other ones are optional.
--
-- > <!--PREAMBLE
-- > postTitle: "Behind Python's unittest.main()"
-- > date: 2010-01-30
-- > draft: true
-- > tags:
-- > - python
-- > -->
--
-- You can mark a post as a draft via the @draft@ variable (it won't be
-- loaded when you call 'loadBlogPosts'), and add tagging (see below) via
-- @tags@. Then, use 'loadBlogPosts' 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' toHtml "layout.html"
-- postLayout <- 'load' toHtml "blog-post.html"
-- posts <- 'loadBlogPosts' "blog/"
-- render (fmap (layout <|| postLayout <|) posts)
-- @
--
-- | 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 <- loadBlogPosts "blog/"
-- @
loadBlogPosts :: FilePath -> PencilApp [Page]
loadBlogPosts fp = do
-- Load posts
postFps <- listDir False fp
-- Sort by date (newest first) and filter out drafts
liftM (filterByVar True "draft" (VBool True /=) . sortByVar "date" dateOrdering)
(mapM (load blogPostUrl) postFps)
-- | Rewrites file path for blog posts.
-- @\/blog\/2011-01-01-the-post-title.html@ => @\/blog\/the-post-title\/@
blogPostUrl :: FilePath -> FilePath
blogPostUrl 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.
--
-- @
-- injectTitle "My Awesome Website" post
-- @
--
-- The above example may insert a @title@ variable with the value @"How to do X
-- - My Awesome Website"@.
--
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
type Tag = T.Text
-- | 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 <- load (fpf tag) tagPageFp
let tagEnv = (insertPages pagesVar taggedPosts . insertText "tag" tag . merge (getPageEnv tagPage)) env
return $ H.insert tag (setPageEnv tagEnv tagPage) acc
)
H.empty
(H.toList tagMap)
-- | Inject the given tagging map into the given @Page@'s environment, as the
-- @tags@ variable, whose value is a @VEnvList@.
injectTagsEnv :: H.HashMap Tag Page -> Page -> Page
injectTagsEnv 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 insertEnv "tags" (VEnvList envs) (getPageEnv page)
in setPageEnv env' page