hakyll-dir-list-1.0.0.1: src/Hakyll/Web/Template/DirList.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module Hakyll.Web.Template.DirList
(
Configuration (..)
, defaultConfiguration
, metadataConfiguration
, dirListField
) where
--import Data.Monoid (mappend)
import Control.Monad (liftM)
import Data.List (sortBy)
import Data.Ord (comparing)
import Hakyll ( MonadMetadata, Identifier
, Item, itemIdentifier, field
, getMetadata, lookupString, listField
, toFilePath, Metadata, Compiler, Context
, splitAll)
import System.FilePath ( dropExtensions
, splitDirectories
, takeBaseName)
import Data.Maybe ( fromMaybe)
import qualified Data.Map as M
import Data.Default (Default (..))
-- | Configuration for allowing variation of collection and
-- item tags in dependency of the hierarchical directory level.
-- The level count begins with zero.
data Configuration = Configuration
{
-- | begin of item tag, default returns '<li>'
beginItemTag :: Int -> String
-- | end of item tag, default returns '</li>'
, endItemTag :: Int -> String
-- | begin of collection tag, default returns '<ul>'
, beginCollectionTag :: Int -> String
-- | end of collection tag, default returns '</ul>'
, endCollectionTag :: Int -> String
}
instance Default Configuration where
def = defaultConfiguration
-- | Default configuration for dirListField
defaultConfiguration :: Configuration
defaultConfiguration = Configuration
{ beginItemTag = \_ -> "<li>"
, endItemTag = \_ -> "</li>"
, beginCollectionTag = \_ -> "<ul>"
, endCollectionTag = \_ -> "</ul>"
}
-- | Read Configuration from Metadata
-- the given default Configuration is used
-- if no coresponding metadata field is found.
-- The fields are: beginItemTag, endItemTag,
-- beginCollectionTag, endCollectionTag
-- These fields can hold a list of tags which are
-- separated by the value of the field tagDelimiter,
-- or if not given an ','. The list holds the
-- tags per level, if level is higher than the
-- tags in the list the last tag is used.
--
metadataConfiguration :: Metadata -> Configuration -> Configuration
metadataConfiguration md default' =
Configuration
( f "beginItemTag" ( beginItemTag default' ) )
( f "endItemTag" ( endItemTag default' ) )
( f "beginCollectionTag" ( beginCollectionTag default' ) )
( f "endCollectionTag" ( endCollectionTag default' ) )
where
del = fromMaybe "," (lookupString "tagDelimiter" md)
f :: String -> ( Int -> String ) -> ( Int -> String )
f tag df =
case lookupString tag md of
Nothing -> df
Just s -> g ( splitAll del s )
g :: [String] -> Int -> String
g (x:[]) _ = x
g (x:_) 0 = x
g (_:xs) l = g xs (l - 1)
g [] _ = "<???>" -- this should not happen
-- | Sort pages alphabetically.
alphabetical :: MonadMetadata m => [Item a] -> m [Item a]
alphabetical =
sortByM $ getItemPath . itemIdentifier
where
sortByM :: (Monad m, Ord k) => (a -> m k) -> [a] -> m [a]
sortByM f xs = liftM (map fst . sortBy (comparing snd)) $
mapM (\x -> liftM (x,) (f x)) xs
-- | get the path of the item
getItemPath :: MonadMetadata m
=> Identifier -- ^ Input page
-> m FilePath -- ^ Parsed UTCTime
getItemPath id' = return $ toFilePath id'
-- | page-id order
data ItemTree a = ItemTree (Item a) [ItemTree a] String String
type ItemPath a = ( Item a, [FilePath])
itemPath :: [ Item a] -> [ ItemPath a]
itemPath =
map (\i -> ( i, splitDirectories
. dropExtensions . toFilePath . itemIdentifier $ i) )
-- | get all files which belonging to one tree
-- this means the first file name (without extensions) is
-- equal to the base name of the following items.
-- return theses and the rest of the list
getTreeFiles :: [ ItemPath a] -> ( [ItemPath a], [ItemPath a])
getTreeFiles [] = ( [], [])
getTreeFiles (p:ps)
| (length $ snd p) > 1 = getTreeFiles ps -- drop directories without
-- an leading file of the same name
| otherwise = getTreeFiles' ( head . snd $ p ) ([],p:ps)
-- | baseName TreeFiles Rest
getTreeFiles' :: FilePath -> ([ItemPath a], [ItemPath a])
-> ( [ItemPath a], [ItemPath a])
getTreeFiles' _ ( ts, [] ) = ( ts, [])
getTreeFiles' a ( ts, p:ps )
| (head $ snd p ) == a = getTreeFiles' a ( ts ++ [p] , ps)
| otherwise = ( ts, p:ps )
-- | build the tree, the input are only files belonging to this tree
-- | key is the order
buildTree :: MonadMetadata m => String -> [ItemPath a] -> m (ItemTree a)
buildTree parentPid (p:ps) = do
pid <- getItemPageId id'
ord <- getItemPageOrder id'
tl <- buildOrderedTreeList (parentPid' ++ pid) ( map (\x->(fst x, tail $ snd x)) ps)
return $ ItemTree (fst p) tl (parentPid' ++ pid) ord
where
id' = itemIdentifier $ fst p
parentPid' = if parentPid == "" then "" else parentPid ++ "-"
buildTree _ [] = error "buildTree: empty file list"
-- -- | build the tree, the input are only files belonging to this tree
-- -- | key is the order
-- buildTree :: MonadMetadata m => String -> [Iid <- getItemPageId id
-- ord <- getItemPageOrder id
-- tl <- buildOrderedTreeList (parentPid' ++ pid) ( map (\x->(fst x, tail $ snd x)) ps)
-- return $ ItemTree (fst p) tl (parentPid' ++ pid) ord
-- where
-- id = itemIdentifier $ fst p
-- parentPid' = if parentPid == "" then "" else parentPid ++ "-"
-- | build tree list
buildTreeList :: MonadMetadata m => String -> [ItemPath a] -> m [ItemTree a]
buildTreeList _ [] = return []
buildTreeList parentPid ps = do
t <- buildTree parentPid ts
tl <- buildTreeList parentPid rs
return $ t : tl
where ( ts, rs) = getTreeFiles ps
-- | sort the treeList
buildOrderedTreeList :: MonadMetadata m => String -> [ItemPath a] -> m [ItemTree a]
buildOrderedTreeList _ [] = return []
buildOrderedTreeList parentPid ps = do
tl <- buildTreeList parentPid ps
return $ sortBy (comparing (\( ItemTree _ _ _ o)-> o)) tl
-- | pid btags etags
data TreeContext = TreeContext String String String deriving Show
getItemTreeAList :: Configuration -> ItemTree a -> Int
-> String -> String -> [(Item a, TreeContext)]
getItemTreeAList cfg ( ItemTree i [] pid _ ) level btags etags =
[( i, TreeContext pid (btags ++ (beginItemTag cfg) level)
((endItemTag cfg) level ++ etags))]
getItemTreeAList cfg ( ItemTree i ts pid _ ) level btags etags =
( i, TreeContext pid (btags ++ (beginItemTag cfg) level) "")
: (getItemTreeListAList cfg (level+1) ((endItemTag cfg) level ++ etags) ts)
--
callGetItemTreeListAList :: Configuration -> [ ItemTree a ]
-> [(Item a, TreeContext)]
callGetItemTreeListAList cfg = getItemTreeListAList cfg 0 ""
--
getItemTreeListAList :: Configuration -> Int -> String
-> [ ItemTree a ] -> [(Item a, TreeContext)]
getItemTreeListAList _ _ _ [] = []
getItemTreeListAList cfg level etags (t:[]) = -- begin + end
getItemTreeAList cfg t level
((beginCollectionTag cfg) level)
((endCollectionTag cfg) level ++ etags)
getItemTreeListAList cfg level etags (t:ts) = -- begin item
(getItemTreeAList cfg t level ((beginCollectionTag cfg) level) "")
++ (getItemTreeListAList' cfg level
((endCollectionTag cfg) level ++ etags) ts)
-- iteration until the end
getItemTreeListAList' :: Configuration -> Int -> String
-> [ ItemTree a ] -> [(Item a, TreeContext)]
getItemTreeListAList' _ _ _ [] = []
getItemTreeListAList' cfg level etags (t:[]) = -- the last item
getItemTreeAList cfg t level "" etags
getItemTreeListAList' cfg level etags (t:ts) = -- the items in the middle
( getItemTreeAList cfg t level "" "")
++ ( getItemTreeListAList' cfg level etags ts)
-- metadata: page-id page-order
-- context: full-page-id
getItemPageId :: MonadMetadata m => Identifier -> m String
getItemPageId id' = do
metadata <- getMetadata id'
return $ fromMaybe
( takeBaseName $ toFilePath id' )
( lookupString "page-id" metadata )
getItemPageOrder :: MonadMetadata m => Identifier -> m String
getItemPageOrder id' = do
metadata <- getMetadata id'
pageId <- getItemPageId id'
return $ fromMaybe pageId ( lookupString "page-order" metadata)
{-|
The exported 'dirListField' function is similar to the
'Hakyll.Web.Template.listField' template function but creates
additional context information which can be used in the template
to create a hierarchical menu.
== Context usable inside the template
[@$begin-tags$@]: injects @\<li\>@ and @\<ul\>@ tags if apropriate
[@$end-tags$@]: contains the corresponding @\<\/li\>@ and @\<\/ul\>@ tags
[@$full-page-id$@]: is the hyphen seperated path of the page
== Metainformation in the source files
For each subdirectory which should be processed one source file
with the same base name should
exist which can contain meta information:
[@pages\/a.md@]: top page for directory a
[@pages\/a\/foo.md@]: page foo within a
The following meta information can be given
[@page-id@]: part of the generated id, if not given the base name of the file
[@page-order@]: give an ordering key for sorting in the current
directory level, if not given the @page-id@ will be used
-}
dirListField :: Configuration -> String -> Context a
-> Compiler [Item a] -> Context b
dirListField cfg key c xs = listField key ( c' `mappend` c) pages'
where
pages = alphabetical =<< xs
treeList = (buildOrderedTreeList "") =<<
map (\ip-> (fst ip, tail . snd $ ip)) <$> itemPath <$> pages
aList = callGetItemTreeListAList cfg <$> treeList
pages' = (map fst) <$> aList -- the pages in the correct order
aList' = map (\(item,ct)->(itemIdentifier item,ct)) <$> aList
idMap = M.fromList <$> aList'
c' = -- ( field "test" ( \i -> M.showTree <$> idMap) ) `mappend`
( field "full-page-id"
( \i -> ( (\(Just (TreeContext pid _ _))->pid)
. ( M.lookup (itemIdentifier i) ) ) <$> idMap ) ) `mappend`
( field "begin-tags"
( \i -> ( (\(Just (TreeContext _ b _))->b)
. ( M.lookup (itemIdentifier i) ) ) <$> idMap ) ) `mappend`
( field "end-tags"
( \i -> ( (\(Just (TreeContext _ _ e))->e)
. ( M.lookup (itemIdentifier i) ) ) <$> idMap ) )