-- | Module containing some specialized functions to deal with tags. -- This Module follows certain conventions. My advice is to stick with them if -- possible. -- -- More concrete: all functions in this module assume that the tags are -- located in the @tags@ field, and separated by commas. An example file -- @foo.markdown@ could look like: -- -- > --- -- > author: Philip K. Dick -- > title: Do androids dream of electric sheep? -- > tags: future, science fiction, humanoid -- > --- -- > The novel is set in a post-apocalyptic near future, where the Earth and -- > its populations have been damaged greatly by Nuclear... -- -- All the following functions would work with such a format. In addition to -- tags, Hakyll also supports categories. The convention when using categories -- is to place pages in subdirectories. -- -- An example, the page @posts\/coding\/2010-01-28-hakyll-categories.markdown@ -- Tags or categories are read using the @readTags@ and @readCategory@ -- functions. This module only provides functions to work with tags: -- categories are represented as tags. This is perfectly possible: categories -- only have an additional restriction that a page can only have one category -- (instead of multiple tags). -- {-# LANGUAGE DeriveDataTypeable, OverloadedStrings, Arrows #-} module Hakyll.Web.Tags ( Tags (..) , readTagsWith , readTags , readCategory , renderTagCloud , renderTagList , renderTagsField , renderCategoryField ) where import Prelude hiding (id) import Control.Category (id) import Control.Applicative ((<$>)) import qualified Data.Map as M import Data.List (intersperse, intercalate) import Control.Arrow (arr, (&&&), (>>>), (***), (<<^), returnA) import Data.Maybe (catMaybes, fromMaybe) import Data.Monoid (mconcat) import Data.Typeable (Typeable) import Data.Binary (Binary, get, put) import Text.Blaze.Renderer.String (renderHtml) import Text.Blaze ((!), toHtml, toValue) import qualified Text.Blaze.Html5 as H import qualified Text.Blaze.Html5.Attributes as A import Hakyll.Web.Page import Hakyll.Web.Page.Metadata import Hakyll.Web.Urls import Hakyll.Core.Writable import Hakyll.Core.Identifier import Hakyll.Core.Compiler import Hakyll.Core.Util.String -- | Data about tags -- data Tags a = Tags { tagsMap :: [(String, [Page a])] } deriving (Show, Typeable) instance Binary a => Binary (Tags a) where get = Tags <$> get put (Tags m) = put m instance Writable (Tags a) where write _ _ = return () -- | Obtain tags from a page -- getTags :: Page a -> [String] getTags = map trim . splitAll "," . getField "tags" -- | Obtain categories from a page -- getCategory :: Page a -> [String] getCategory = return . getField "category" -- | Higher-level function to read tags -- readTagsWith :: (Page a -> [String]) -- ^ Function extracting tags from a page -> [Page a] -- ^ Pages -> Tags a -- ^ Resulting tags readTagsWith f pages = Tags { tagsMap = M.toList $ foldl (M.unionWith (++)) M.empty (map readTagsWith' pages) } where -- Create a tag map for one page readTagsWith' page = let tags = f page in M.fromList $ zip tags $ repeat [page] -- | Read a tagmap using the @tags@ metadata field -- readTags :: [Page a] -> Tags a readTags = readTagsWith getTags -- | Read a tagmap using the @category@ metadata field -- readCategory :: [Page a] -> Tags a readCategory = readTagsWith getCategory -- | Render tags in HTML -- renderTags :: (String -> Identifier (Page a)) -- ^ Produce a link -> (String -> String -> Int -> Int -> Int -> String) -- ^ Produce a tag item: tag, url, count, min count, max count -> ([String] -> String) -- ^ Join items -> Compiler (Tags a) String -- ^ Tag cloud renderer renderTags makeUrl makeItem concatItems = proc (Tags tags) -> do -- In tags' we create a list: [((tag, route), count)] tags' <- mapCompiler ((id &&& (getRouteFor <<^ makeUrl)) *** arr length) -< tags let -- Absolute frequencies of the pages freqs = map snd tags' -- The minimum and maximum count found (min', max') | null freqs = (0, 1) | otherwise = (minimum &&& maximum) freqs -- Create a link for one item makeItem' ((tag, url), count) = makeItem tag (toUrl $ fromMaybe "/" url) count min' max' -- Render and return the HTML returnA -< concatItems $ map makeItem' tags' -- | Render a tag cloud in HTML -- renderTagCloud :: (String -> Identifier (Page a)) -- ^ Produce a link for a tag -> Double -- ^ Smallest font size, in percent -> Double -- ^ Biggest font size, in percent -> Compiler (Tags a) String -- ^ Tag cloud renderer renderTagCloud makeUrl minSize maxSize = renderTags makeUrl makeLink (intercalate " ") where makeLink tag url count min' max' = renderHtml $ H.a ! A.style (toValue $ "font-size: " ++ size count min' max') ! A.href (toValue url) $ toHtml tag -- Show the relative size of one 'count' in percent size count min' max' = let diff = 1 + fromIntegral max' - fromIntegral min' relative = (fromIntegral count - fromIntegral min') / diff size' = floor $ minSize + relative * (maxSize - minSize) in show (size' :: Int) ++ "%" -- | Render a simple tag list in HTML, with the tag count next to the item -- renderTagList :: (String -> Identifier (Page a)) -> Compiler (Tags a) (String) renderTagList makeUrl = renderTags makeUrl makeLink (intercalate ", ") where makeLink tag url count _ _ = renderHtml $ H.a ! A.href (toValue url) $ toHtml (tag ++ " (" ++ show count ++ ")") -- | Render tags with links -- renderTagsFieldWith :: (Page a -> [String]) -- ^ Function to get the tags -> String -- ^ Destination key -> (String -> Identifier a) -- ^ Create a link for a tag -> Compiler (Page a) (Page a) -- ^ Resulting compiler renderTagsFieldWith tags destination makeUrl = id &&& arr tags >>> setFieldA destination renderTags' where -- Compiler creating a comma-separated HTML string for a list of tags renderTags' :: Compiler [String] String renderTags' = arr (map $ id &&& makeUrl) >>> mapCompiler (id *** getRouteFor) >>> arr (map $ uncurry renderLink) >>> arr (renderHtml . mconcat . intersperse ", " . catMaybes) -- Render one tag link renderLink _ Nothing = Nothing renderLink tag (Just filePath) = Just $ H.a ! A.href (toValue $ toUrl filePath) $ toHtml tag -- | Render tags with links -- renderTagsField :: String -- ^ Destination key -> (String -> Identifier a) -- ^ Create a link for a tag -> Compiler (Page a) (Page a) -- ^ Resulting compiler renderTagsField = renderTagsFieldWith getTags -- | Render the category in a link -- renderCategoryField :: String -- ^ Destination key -> (String -> Identifier a) -- ^ Create a category link -> Compiler (Page a) (Page a) -- ^ Resulting compiler renderCategoryField = renderTagsFieldWith getCategory