diff options
author | Jasper Van der Jeugt <m@jaspervdj.be> | 2017-06-19 11:57:23 +0200 |
---|---|---|
committer | Jasper Van der Jeugt <m@jaspervdj.be> | 2017-06-19 11:57:23 +0200 |
commit | 67ecff7ad383640bc73d64edc2506c7cc648a134 (patch) | |
tree | 6d328e43c3ab86c29a2d775fabaa23618c16fb51 /lib/Hakyll/Web | |
parent | 2df3209bafa08e6b77ee4a8598fc503269513527 (diff) | |
download | hakyll-67ecff7ad383640bc73d64edc2506c7cc648a134.tar.gz |
Move src/ to lib/, put Init.hs in src/
Diffstat (limited to 'lib/Hakyll/Web')
-rw-r--r-- | lib/Hakyll/Web/CompressCss.hs | 86 | ||||
-rw-r--r-- | lib/Hakyll/Web/Feed.hs | 135 | ||||
-rw-r--r-- | lib/Hakyll/Web/Html.hs | 184 | ||||
-rw-r--r-- | lib/Hakyll/Web/Html/RelativizeUrls.hs | 52 | ||||
-rw-r--r-- | lib/Hakyll/Web/Paginate.hs | 153 | ||||
-rw-r--r-- | lib/Hakyll/Web/Pandoc.hs | 164 | ||||
-rw-r--r-- | lib/Hakyll/Web/Pandoc/Biblio.hs | 115 | ||||
-rw-r--r-- | lib/Hakyll/Web/Pandoc/Binary.hs | 32 | ||||
-rw-r--r-- | lib/Hakyll/Web/Pandoc/FileType.hs | 74 | ||||
-rw-r--r-- | lib/Hakyll/Web/Redirect.hs | 87 | ||||
-rw-r--r-- | lib/Hakyll/Web/Tags.hs | 344 | ||||
-rw-r--r-- | lib/Hakyll/Web/Template.hs | 154 | ||||
-rw-r--r-- | lib/Hakyll/Web/Template/Context.hs | 379 | ||||
-rw-r--r-- | lib/Hakyll/Web/Template/Internal.hs | 203 | ||||
-rw-r--r-- | lib/Hakyll/Web/Template/Internal/Element.hs | 298 | ||||
-rw-r--r-- | lib/Hakyll/Web/Template/Internal/Trim.hs | 95 | ||||
-rw-r--r-- | lib/Hakyll/Web/Template/List.hs | 91 |
17 files changed, 2646 insertions, 0 deletions
diff --git a/lib/Hakyll/Web/CompressCss.hs b/lib/Hakyll/Web/CompressCss.hs new file mode 100644 index 0000000..9f61534 --- /dev/null +++ b/lib/Hakyll/Web/CompressCss.hs @@ -0,0 +1,86 @@ +-------------------------------------------------------------------------------- +-- | Module used for CSS compression. The compression is currently in a simple +-- state, but would typically reduce the number of bytes by about 25%. +module Hakyll.Web.CompressCss + ( compressCssCompiler + , compressCss + ) where + + +-------------------------------------------------------------------------------- +import Data.List (isPrefixOf) + + +-------------------------------------------------------------------------------- +import Hakyll.Core.Compiler +import Hakyll.Core.Item + + +-------------------------------------------------------------------------------- +-- | Compiler form of 'compressCss' +compressCssCompiler :: Compiler (Item String) +compressCssCompiler = fmap compressCss <$> getResourceString + + +-------------------------------------------------------------------------------- +-- | Compress CSS to speed up your site. +compressCss :: String -> String +compressCss = compressSeparators . stripComments . compressWhitespace + + +-------------------------------------------------------------------------------- +-- | Compresses certain forms of separators. +compressSeparators :: String -> String +compressSeparators [] = [] +compressSeparators str + | isConstant = head str : retainConstants compressSeparators (head str) (drop 1 str) + | stripFirst = compressSeparators (drop 1 str) + | stripSecond = compressSeparators (head str : (drop 2 str)) + | otherwise = head str : compressSeparators (drop 1 str) + where + isConstant = or $ map (isOfPrefix str) ["\"", "'"] + stripFirst = or $ map (isOfPrefix str) $ [";;", ";}"] ++ (map (\c -> " " ++ c) separators) + stripSecond = or $ map (isOfPrefix str) $ map (\c -> c ++ " ") separators + separators = [" ", "{", "}", ":", ";", ",", ">", "+", "!"] + +-------------------------------------------------------------------------------- +-- | Compresses all whitespace. +compressWhitespace :: String -> String +compressWhitespace [] = [] +compressWhitespace str + | isConstant = head str : retainConstants compressWhitespace (head str) (drop 1 str) + | replaceOne = compressWhitespace (' ' : (drop 1 str)) + | replaceTwo = compressWhitespace (' ' : (drop 2 str)) + | otherwise = head str : compressWhitespace (drop 1 str) + where + isConstant = or $ map (isOfPrefix str) ["\"", "'"] + replaceOne = or $ map (isOfPrefix str) ["\t", "\n", "\r"] + replaceTwo = or $ map (isOfPrefix str) [" \t", " \n", " \r", " "] + +-------------------------------------------------------------------------------- +-- | Function that strips CSS comments away. +stripComments :: String -> String +stripComments [] = [] +stripComments str + | isConstant = head str : retainConstants stripComments (head str) (drop 1 str) + | isPrefixOf "/*" str = stripComments $ eatComments $ drop 2 str + | otherwise = head str : stripComments (drop 1 str) + where + isConstant = or $ map (isOfPrefix str) ["\"", "'"] + eatComments str' + | null str' = [] + | isPrefixOf "*/" str' = drop 2 str' + | otherwise = eatComments $ drop 1 str' + +-------------------------------------------------------------------------------- +-- | Helper function to handle string constants correctly. +retainConstants :: (String -> String) -> Char -> String -> String +retainConstants f delim str + | null str = [] + | isPrefixOf [delim] str = head str : f (drop 1 str) + | otherwise = head str : retainConstants f delim (drop 1 str) + +-------------------------------------------------------------------------------- +-- | Helper function to determine whether a string is a substring. +isOfPrefix :: String -> String -> Bool +isOfPrefix = flip isPrefixOf diff --git a/lib/Hakyll/Web/Feed.hs b/lib/Hakyll/Web/Feed.hs new file mode 100644 index 0000000..6c6fa76 --- /dev/null +++ b/lib/Hakyll/Web/Feed.hs @@ -0,0 +1,135 @@ +-------------------------------------------------------------------------------- +-- | A Module that allows easy rendering of RSS feeds. +-- +-- The main rendering functions (@renderRss@, @renderAtom@) all assume that +-- you pass the list of items so that the most recent entry in the feed is the +-- first item in the list. +-- +-- Also note that the context should have (at least) the following fields to +-- produce a correct feed: +-- +-- - @$title$@: Title of the item +-- +-- - @$description$@: Description to appear in the feed +-- +-- - @$url$@: URL to the item - this is usually set automatically. +-- +-- In addition, the posts should be named according to the rules for +-- 'Hakyll.Web.Template.Context.dateField' +module Hakyll.Web.Feed + ( FeedConfiguration (..) + , renderRss + , renderAtom + ) where + + +-------------------------------------------------------------------------------- +import Hakyll.Core.Compiler +import Hakyll.Core.Compiler.Internal +import Hakyll.Core.Item +import Hakyll.Core.Util.String (replaceAll) +import Hakyll.Web.Template +import Hakyll.Web.Template.Context +import Hakyll.Web.Template.List + + +-------------------------------------------------------------------------------- +import Paths_hakyll + + +-------------------------------------------------------------------------------- +-- | This is a data structure to keep the configuration of a feed. +data FeedConfiguration = FeedConfiguration + { -- | Title of the feed. + feedTitle :: String + , -- | Description of the feed. + feedDescription :: String + , -- | Name of the feed author. + feedAuthorName :: String + , -- | Email of the feed author. + feedAuthorEmail :: String + , -- | Absolute root URL of the feed site (e.g. @http://jaspervdj.be@) + feedRoot :: String + } deriving (Show, Eq) + + +-------------------------------------------------------------------------------- +-- | Abstract function to render any feed. +renderFeed :: FilePath -- ^ Feed template + -> FilePath -- ^ Item template + -> FeedConfiguration -- ^ Feed configuration + -> Context String -- ^ Context for the items + -> [Item String] -- ^ Input items + -> Compiler (Item String) -- ^ Resulting item +renderFeed feedPath itemPath config itemContext items = do + feedTpl <- loadTemplate feedPath + itemTpl <- loadTemplate itemPath + + protectedItems <- mapM (applyFilter protectCDATA) items + body <- makeItem =<< applyTemplateList itemTpl itemContext' protectedItems + applyTemplate feedTpl feedContext body + where + applyFilter :: (Monad m,Functor f) => (String -> String) -> f String -> m (f String) + applyFilter tr str = return $ fmap tr str + protectCDATA :: String -> String + protectCDATA = replaceAll "]]>" (const "]]>") + -- Auxiliary: load a template from a datafile + loadTemplate path = do + file <- compilerUnsafeIO $ getDataFileName path + unsafeReadTemplateFile file + + itemContext' = mconcat + [ itemContext + , constField "root" (feedRoot config) + , constField "authorName" (feedAuthorName config) + , constField "authorEmail" (feedAuthorEmail config) + ] + + feedContext = mconcat + [ bodyField "body" + , constField "title" (feedTitle config) + , constField "description" (feedDescription config) + , constField "authorName" (feedAuthorName config) + , constField "authorEmail" (feedAuthorEmail config) + , constField "root" (feedRoot config) + , urlField "url" + , updatedField + , missingField + ] + + -- Take the first "updated" field from all items -- this should be the most + -- recent. + updatedField = field "updated" $ \_ -> case items of + [] -> return "Unknown" + (x : _) -> unContext itemContext' "updated" [] x >>= \cf -> case cf of + ListField _ _ -> fail "Hakyll.Web.Feed.renderFeed: Internal error" + StringField s -> return s + + +-------------------------------------------------------------------------------- +-- | Render an RSS feed with a number of items. +renderRss :: FeedConfiguration -- ^ Feed configuration + -> Context String -- ^ Item context + -> [Item String] -- ^ Feed items + -> Compiler (Item String) -- ^ Resulting feed +renderRss config context = renderFeed + "templates/rss.xml" "templates/rss-item.xml" config + (makeItemContext "%a, %d %b %Y %H:%M:%S UT" context) + + +-------------------------------------------------------------------------------- +-- | Render an Atom feed with a number of items. +renderAtom :: FeedConfiguration -- ^ Feed configuration + -> Context String -- ^ Item context + -> [Item String] -- ^ Feed items + -> Compiler (Item String) -- ^ Resulting feed +renderAtom config context = renderFeed + "templates/atom.xml" "templates/atom-item.xml" config + (makeItemContext "%Y-%m-%dT%H:%M:%SZ" context) + + +-------------------------------------------------------------------------------- +-- | Copies @$updated$@ from @$published$@ if it is not already set. +makeItemContext :: String -> Context a -> Context a +makeItemContext fmt context = mconcat + [dateField "published" fmt, context, dateField "updated" fmt] diff --git a/lib/Hakyll/Web/Html.hs b/lib/Hakyll/Web/Html.hs new file mode 100644 index 0000000..6b7ec88 --- /dev/null +++ b/lib/Hakyll/Web/Html.hs @@ -0,0 +1,184 @@ +-------------------------------------------------------------------------------- +-- | Provides utilities to manipulate HTML pages +module Hakyll.Web.Html + ( -- * Generic + withTags + + -- * Headers + , demoteHeaders + + -- * Url manipulation + , getUrls + , withUrls + , toUrl + , toSiteRoot + , isExternal + + -- * Stripping/escaping + , stripTags + , escapeHtml + ) where + + +-------------------------------------------------------------------------------- +import Data.Char (digitToInt, intToDigit, + isDigit, toLower) +import Data.List (isPrefixOf) +import qualified Data.Set as S +import System.FilePath.Posix (joinPath, splitPath, + takeDirectory) +import Text.Blaze.Html (toHtml) +import Text.Blaze.Html.Renderer.String (renderHtml) +import qualified Text.HTML.TagSoup as TS +import Network.URI (isUnreserved, escapeURIString) + + +-------------------------------------------------------------------------------- +-- | Map over all tags in the document +withTags :: (TS.Tag String -> TS.Tag String) -> String -> String +withTags f = renderTags' . map f . parseTags' + + +-------------------------------------------------------------------------------- +-- | Map every @h1@ to an @h2@, @h2@ to @h3@, etc. +demoteHeaders :: String -> String +demoteHeaders = withTags $ \tag -> case tag of + TS.TagOpen t a -> TS.TagOpen (demote t) a + TS.TagClose t -> TS.TagClose (demote t) + t -> t + where + demote t@['h', n] + | isDigit n = ['h', intToDigit (min 6 $ digitToInt n + 1)] + | otherwise = t + demote t = t + + +-------------------------------------------------------------------------------- +isUrlAttribute :: String -> Bool +isUrlAttribute = (`elem` ["src", "href", "data", "poster"]) + + +-------------------------------------------------------------------------------- +getUrls :: [TS.Tag String] -> [String] +getUrls tags = [v | TS.TagOpen _ as <- tags, (k, v) <- as, isUrlAttribute k] + + +-------------------------------------------------------------------------------- +-- | Apply a function to each URL on a webpage +withUrls :: (String -> String) -> String -> String +withUrls f = withTags tag + where + tag (TS.TagOpen s a) = TS.TagOpen s $ map attr a + tag x = x + attr (k, v) = (k, if isUrlAttribute k then f v else v) + + +-------------------------------------------------------------------------------- +-- | Customized TagSoup renderer. The default TagSoup renderer escape CSS +-- within style tags, and doesn't properly minimize. +renderTags' :: [TS.Tag String] -> String +renderTags' = TS.renderTagsOptions TS.RenderOptions + { TS.optRawTag = (`elem` ["script", "style"]) . map toLower + , TS.optMinimize = (`S.member` minimize) . map toLower + , TS.optEscape = id + } + where + -- A list of elements which must be minimized + minimize = S.fromList + [ "area", "br", "col", "embed", "hr", "img", "input", "meta", "link" + , "param" + ] + + +-------------------------------------------------------------------------------- +-- | Customized TagSoup parser: do not decode any entities. +parseTags' :: String -> [TS.Tag String] +parseTags' = TS.parseTagsOptions (TS.parseOptions :: TS.ParseOptions String) + { TS.optEntityData = \(str, b) -> [TS.TagText $ "&" ++ str ++ [';' | b]] + , TS.optEntityAttrib = \(str, b) -> ("&" ++ str ++ [';' | b], []) + } + + +-------------------------------------------------------------------------------- +-- | Convert a filepath to an URL starting from the site root +-- +-- Example: +-- +-- > toUrl "foo/bar.html" +-- +-- Result: +-- +-- > "/foo/bar.html" +-- +-- This also sanitizes the URL, e.g. converting spaces into '%20' +toUrl :: FilePath -> String +toUrl url = case url of + ('/' : xs) -> '/' : sanitize xs + xs -> '/' : sanitize xs + where + -- Everything but unreserved characters should be escaped as we are + -- sanitising the path therefore reserved characters which have a + -- meaning in URI does not appear. Special casing for `/`, because it has + -- a special meaning in FilePath as well as in URI. + sanitize = escapeURIString (\c -> c == '/' || isUnreserved c) + + +-------------------------------------------------------------------------------- +-- | Get the relative url to the site root, for a given (absolute) url +toSiteRoot :: String -> String +toSiteRoot = emptyException . joinPath . map parent + . filter relevant . splitPath . takeDirectory + where + parent = const ".." + emptyException [] = "." + emptyException x = x + relevant "." = False + relevant "/" = False + relevant "./" = False + relevant _ = True + + +-------------------------------------------------------------------------------- +-- | Check if an URL links to an external HTTP(S) source +isExternal :: String -> Bool +isExternal url = any (flip isPrefixOf url) ["http://", "https://", "//"] + + +-------------------------------------------------------------------------------- +-- | Strip all HTML tags from a string +-- +-- Example: +-- +-- > stripTags "<p>foo</p>" +-- +-- Result: +-- +-- > "foo" +-- +-- This also works for incomplete tags +-- +-- Example: +-- +-- > stripTags "<p>foo</p" +-- +-- Result: +-- +-- > "foo" +stripTags :: String -> String +stripTags [] = [] +stripTags ('<' : xs) = stripTags $ drop 1 $ dropWhile (/= '>') xs +stripTags (x : xs) = x : stripTags xs + + +-------------------------------------------------------------------------------- +-- | HTML-escape a string +-- +-- Example: +-- +-- > escapeHtml "Me & Dean" +-- +-- Result: +-- +-- > "Me & Dean" +escapeHtml :: String -> String +escapeHtml = renderHtml . toHtml diff --git a/lib/Hakyll/Web/Html/RelativizeUrls.hs b/lib/Hakyll/Web/Html/RelativizeUrls.hs new file mode 100644 index 0000000..33b0c2c --- /dev/null +++ b/lib/Hakyll/Web/Html/RelativizeUrls.hs @@ -0,0 +1,52 @@ +-------------------------------------------------------------------------------- +-- | This module exposes a function which can relativize URL's on a webpage. +-- +-- This means that one can deploy the resulting site on +-- @http:\/\/example.com\/@, but also on @http:\/\/example.com\/some-folder\/@ +-- without having to change anything (simply copy over the files). +-- +-- To use it, you should use absolute URL's from the site root everywhere. For +-- example, use +-- +-- > <img src="/images/lolcat.png" alt="Funny zomgroflcopter" /> +-- +-- in a blogpost. When running this through the relativize URL's module, this +-- will result in (suppose your blogpost is located at @\/posts\/foo.html@: +-- +-- > <img src="../images/lolcat.png" alt="Funny zomgroflcopter" /> +module Hakyll.Web.Html.RelativizeUrls + ( relativizeUrls + , relativizeUrlsWith + ) where + + +-------------------------------------------------------------------------------- +import Data.List (isPrefixOf) + + +-------------------------------------------------------------------------------- +import Hakyll.Core.Compiler +import Hakyll.Core.Item +import Hakyll.Web.Html + + +-------------------------------------------------------------------------------- +-- | Compiler form of 'relativizeUrls' which automatically picks the right root +-- path +relativizeUrls :: Item String -> Compiler (Item String) +relativizeUrls item = do + route <- getRoute $ itemIdentifier item + return $ case route of + Nothing -> item + Just r -> fmap (relativizeUrlsWith $ toSiteRoot r) item + + +-------------------------------------------------------------------------------- +-- | Relativize URL's in HTML +relativizeUrlsWith :: String -- ^ Path to the site root + -> String -- ^ HTML to relativize + -> String -- ^ Resulting HTML +relativizeUrlsWith root = withUrls rel + where + isRel x = "/" `isPrefixOf` x && not ("//" `isPrefixOf` x) + rel x = if isRel x then root ++ x else x diff --git a/lib/Hakyll/Web/Paginate.hs b/lib/Hakyll/Web/Paginate.hs new file mode 100644 index 0000000..dd058f6 --- /dev/null +++ b/lib/Hakyll/Web/Paginate.hs @@ -0,0 +1,153 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE OverloadedStrings #-} +module Hakyll.Web.Paginate + ( PageNumber + , Paginate (..) + , buildPaginateWith + , paginateEvery + , paginateRules + , paginateContext + ) where + + +-------------------------------------------------------------------------------- +import Control.Applicative (empty) +import Control.Monad (forM_, forM) +import qualified Data.Map as M +import qualified Data.Set as S + + +-------------------------------------------------------------------------------- +import Hakyll.Core.Compiler +import Hakyll.Core.Identifier +import Hakyll.Core.Identifier.Pattern +import Hakyll.Core.Item +import Hakyll.Core.Metadata +import Hakyll.Core.Rules +import Hakyll.Web.Html +import Hakyll.Web.Template.Context + + +-------------------------------------------------------------------------------- +type PageNumber = Int + + +-------------------------------------------------------------------------------- +-- | Data about paginators +data Paginate = Paginate + { paginateMap :: M.Map PageNumber [Identifier] + , paginateMakeId :: PageNumber -> Identifier + , paginateDependency :: Dependency + } + + +-------------------------------------------------------------------------------- +paginateNumPages :: Paginate -> Int +paginateNumPages = M.size . paginateMap + + +-------------------------------------------------------------------------------- +paginateEvery :: Int -> [a] -> [[a]] +paginateEvery n = go + where + go [] = [] + go xs = let (y, ys) = splitAt n xs in y : go ys + + +-------------------------------------------------------------------------------- +buildPaginateWith + :: MonadMetadata m + => ([Identifier] -> m [[Identifier]]) -- ^ Group items into pages + -> Pattern -- ^ Select items to paginate + -> (PageNumber -> Identifier) -- ^ Identifiers for the pages + -> m Paginate +buildPaginateWith grouper pattern makeId = do + ids <- getMatches pattern + idGroups <- grouper ids + let idsSet = S.fromList ids + return Paginate + { paginateMap = M.fromList (zip [1 ..] idGroups) + , paginateMakeId = makeId + , paginateDependency = PatternDependency pattern idsSet + } + + +-------------------------------------------------------------------------------- +paginateRules :: Paginate -> (PageNumber -> Pattern -> Rules ()) -> Rules () +paginateRules paginator rules = + forM_ (M.toList $ paginateMap paginator) $ \(idx, identifiers) -> + rulesExtraDependencies [paginateDependency paginator] $ + create [paginateMakeId paginator idx] $ + rules idx $ fromList identifiers + + +-------------------------------------------------------------------------------- +-- | Get the identifier for a certain page by passing in the page number. +paginatePage :: Paginate -> PageNumber -> Maybe Identifier +paginatePage pag pageNumber + | pageNumber < 1 = Nothing + | pageNumber > (paginateNumPages pag) = Nothing + | otherwise = Just $ paginateMakeId pag pageNumber + + +-------------------------------------------------------------------------------- +-- | A default paginate context which provides the following keys: +-- +-- +-- * @firstPageNum@ +-- * @firstPageUrl@ +-- * @previousPageNum@ +-- * @previousPageUrl@ +-- * @nextPageNum@ +-- * @nextPageUrl@ +-- * @lastPageNum@ +-- * @lastPageUrl@ +-- * @currentPageNum@ +-- * @currentPageUrl@ +-- * @numPages@ +-- * @allPages@ +paginateContext :: Paginate -> PageNumber -> Context a +paginateContext pag currentPage = mconcat + [ field "firstPageNum" $ \_ -> otherPage 1 >>= num + , field "firstPageUrl" $ \_ -> otherPage 1 >>= url + , field "previousPageNum" $ \_ -> otherPage (currentPage - 1) >>= num + , field "previousPageUrl" $ \_ -> otherPage (currentPage - 1) >>= url + , field "nextPageNum" $ \_ -> otherPage (currentPage + 1) >>= num + , field "nextPageUrl" $ \_ -> otherPage (currentPage + 1) >>= url + , field "lastPageNum" $ \_ -> otherPage lastPage >>= num + , field "lastPageUrl" $ \_ -> otherPage lastPage >>= url + , field "currentPageNum" $ \i -> thisPage i >>= num + , field "currentPageUrl" $ \i -> thisPage i >>= url + , constField "numPages" $ show $ paginateNumPages pag + , Context $ \k _ i -> case k of + "allPages" -> do + let ctx = + field "isCurrent" (\n -> if fst (itemBody n) == currentPage then return "true" else empty) `mappend` + field "num" (num . itemBody) `mappend` + field "url" (url . itemBody) + + list <- forM [1 .. lastPage] $ + \n -> if n == currentPage then thisPage i else otherPage n + items <- mapM makeItem list + return $ ListField ctx items + _ -> do + empty + + ] + where + lastPage = paginateNumPages pag + + thisPage i = return (currentPage, itemIdentifier i) + otherPage n + | n == currentPage = fail $ "This is the current page: " ++ show n + | otherwise = case paginatePage pag n of + Nothing -> fail $ "No such page: " ++ show n + Just i -> return (n, i) + + num :: (Int, Identifier) -> Compiler String + num = return . show . fst + + url :: (Int, Identifier) -> Compiler String + url (n, i) = getRoute i >>= \mbR -> case mbR of + Just r -> return $ toUrl r + Nothing -> fail $ "No URL for page: " ++ show n diff --git a/lib/Hakyll/Web/Pandoc.hs b/lib/Hakyll/Web/Pandoc.hs new file mode 100644 index 0000000..eec0a8a --- /dev/null +++ b/lib/Hakyll/Web/Pandoc.hs @@ -0,0 +1,164 @@ +-------------------------------------------------------------------------------- +-- | Module exporting convenient pandoc bindings +module Hakyll.Web.Pandoc + ( -- * The basic building blocks + readPandoc + , readPandocWith + , writePandoc + , writePandocWith + , renderPandoc + , renderPandocWith + + -- * Derived compilers + , pandocCompiler + , pandocCompilerWith + , pandocCompilerWithTransform + , pandocCompilerWithTransformM + + -- * Default options + , defaultHakyllReaderOptions + , defaultHakyllWriterOptions + ) where + + +-------------------------------------------------------------------------------- +import qualified Data.Set as S +import Text.Pandoc +import Text.Pandoc.Error (PandocError (..)) + + +-------------------------------------------------------------------------------- +import Hakyll.Core.Compiler +import Hakyll.Core.Item +import Hakyll.Web.Pandoc.FileType + + +-------------------------------------------------------------------------------- +-- | Read a string using pandoc, with the default options +readPandoc + :: Item String -- ^ String to read + -> Compiler (Item Pandoc) -- ^ Resulting document +readPandoc = readPandocWith defaultHakyllReaderOptions + + +-------------------------------------------------------------------------------- +-- | Read a string using pandoc, with the supplied options +readPandocWith + :: ReaderOptions -- ^ Parser options + -> Item String -- ^ String to read + -> Compiler (Item Pandoc) -- ^ Resulting document +readPandocWith ropt item = + case traverse (reader ropt (itemFileType item)) item of + Left (ParseFailure err) -> fail $ + "Hakyll.Web.Pandoc.readPandocWith: parse failed: " ++ err + Left (ParsecError _ err) -> fail $ + "Hakyll.Web.Pandoc.readPandocWith: parse failed: " ++ show err + Right item' -> return item' + where + reader ro t = case t of + DocBook -> readDocBook ro + Html -> readHtml ro + LaTeX -> readLaTeX ro + LiterateHaskell t' -> reader (addExt ro Ext_literate_haskell) t' + Markdown -> readMarkdown ro + MediaWiki -> readMediaWiki ro + OrgMode -> readOrg ro + Rst -> readRST ro + Textile -> readTextile ro + _ -> error $ + "Hakyll.Web.readPandocWith: I don't know how to read a file of " ++ + "the type " ++ show t ++ " for: " ++ show (itemIdentifier item) + + addExt ro e = ro {readerExtensions = S.insert e $ readerExtensions ro} + + +-------------------------------------------------------------------------------- +-- | Write a document (as HTML) using pandoc, with the default options +writePandoc :: Item Pandoc -- ^ Document to write + -> Item String -- ^ Resulting HTML +writePandoc = writePandocWith defaultHakyllWriterOptions + + +-------------------------------------------------------------------------------- +-- | Write a document (as HTML) using pandoc, with the supplied options +writePandocWith :: WriterOptions -- ^ Writer options for pandoc + -> Item Pandoc -- ^ Document to write + -> Item String -- ^ Resulting HTML +writePandocWith wopt = fmap $ writeHtmlString wopt + + +-------------------------------------------------------------------------------- +-- | Render the resource using pandoc +renderPandoc :: Item String -> Compiler (Item String) +renderPandoc = + renderPandocWith defaultHakyllReaderOptions defaultHakyllWriterOptions + + +-------------------------------------------------------------------------------- +-- | Render the resource using pandoc +renderPandocWith + :: ReaderOptions -> WriterOptions -> Item String -> Compiler (Item String) +renderPandocWith ropt wopt item = + writePandocWith wopt <$> readPandocWith ropt item + + +-------------------------------------------------------------------------------- +-- | Read a page render using pandoc +pandocCompiler :: Compiler (Item String) +pandocCompiler = + pandocCompilerWith defaultHakyllReaderOptions defaultHakyllWriterOptions + + +-------------------------------------------------------------------------------- +-- | A version of 'pandocCompiler' which allows you to specify your own pandoc +-- options +pandocCompilerWith :: ReaderOptions -> WriterOptions -> Compiler (Item String) +pandocCompilerWith ropt wopt = + cached "Hakyll.Web.Pandoc.pandocCompilerWith" $ + pandocCompilerWithTransform ropt wopt id + + +-------------------------------------------------------------------------------- +-- | An extension of 'pandocCompilerWith' which allows you to specify a custom +-- pandoc transformation for the content +pandocCompilerWithTransform :: ReaderOptions -> WriterOptions + -> (Pandoc -> Pandoc) + -> Compiler (Item String) +pandocCompilerWithTransform ropt wopt f = + pandocCompilerWithTransformM ropt wopt (return . f) + + +-------------------------------------------------------------------------------- +-- | Similar to 'pandocCompilerWithTransform', but the transformation +-- function is monadic. This is useful when you want the pandoc +-- transformation to use the 'Compiler' information such as routes, +-- metadata, etc +pandocCompilerWithTransformM :: ReaderOptions -> WriterOptions + -> (Pandoc -> Compiler Pandoc) + -> Compiler (Item String) +pandocCompilerWithTransformM ropt wopt f = + writePandocWith wopt <$> + (traverse f =<< readPandocWith ropt =<< getResourceBody) + + +-------------------------------------------------------------------------------- +-- | The default reader options for pandoc parsing in hakyll +defaultHakyllReaderOptions :: ReaderOptions +defaultHakyllReaderOptions = def + { -- The following option causes pandoc to read smart typography, a nice + -- and free bonus. + readerSmart = True + } + + +-------------------------------------------------------------------------------- +-- | The default writer options for pandoc rendering in hakyll +defaultHakyllWriterOptions :: WriterOptions +defaultHakyllWriterOptions = def + { -- This option causes literate haskell to be written using '>' marks in + -- html, which I think is a good default. + writerExtensions = S.insert Ext_literate_haskell (writerExtensions def) + , -- We want to have hightlighting by default, to be compatible with earlier + -- Hakyll releases + writerHighlight = True + } diff --git a/lib/Hakyll/Web/Pandoc/Biblio.hs b/lib/Hakyll/Web/Pandoc/Biblio.hs new file mode 100644 index 0000000..dfe6d93 --- /dev/null +++ b/lib/Hakyll/Web/Pandoc/Biblio.hs @@ -0,0 +1,115 @@ +-------------------------------------------------------------------------------- +-- | Wraps pandocs bibiliography handling +-- +-- In order to add a bibliography, you will need a bibliography file (e.g. +-- @.bib@) and a CSL file (@.csl@). Both need to be compiled with their +-- respective compilers ('biblioCompiler' and 'cslCompiler'). Then, you can +-- refer to these files when you use 'readPandocBiblio'. This function also +-- takes the reader options for completeness -- you can use +-- 'defaultHakyllReaderOptions' if you're unsure. +-- 'pandocBiblioCompiler' is a convenience wrapper which works like 'pandocCompiler', +-- but also takes paths to compiled bibliography and csl files. +{-# LANGUAGE Arrows #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +module Hakyll.Web.Pandoc.Biblio + ( CSL + , cslCompiler + , Biblio (..) + , biblioCompiler + , readPandocBiblio + , pandocBiblioCompiler + ) where + + +-------------------------------------------------------------------------------- +import Control.Monad (liftM, replicateM) +import Data.Binary (Binary (..)) +import Data.Default (def) +import Data.Typeable (Typeable) +import Hakyll.Core.Compiler +import Hakyll.Core.Identifier +import Hakyll.Core.Item +import Hakyll.Core.Writable +import Hakyll.Web.Pandoc +import Hakyll.Web.Pandoc.Binary () +import qualified Text.CSL as CSL +import Text.CSL.Pandoc (processCites) +import Text.Pandoc (Pandoc, ReaderOptions (..)) + + +-------------------------------------------------------------------------------- +data CSL = CSL + deriving (Show, Typeable) + + +-------------------------------------------------------------------------------- +instance Binary CSL where + put CSL = return () + get = return CSL + + +-------------------------------------------------------------------------------- +instance Writable CSL where + -- Shouldn't be written. + write _ _ = return () + + +-------------------------------------------------------------------------------- +cslCompiler :: Compiler (Item CSL) +cslCompiler = makeItem CSL + + +-------------------------------------------------------------------------------- +newtype Biblio = Biblio [CSL.Reference] + deriving (Show, Typeable) + + +-------------------------------------------------------------------------------- +instance Binary Biblio where + -- Ugly. + get = do + len <- get + Biblio <$> replicateM len get + put (Biblio rs) = put (length rs) >> mapM_ put rs + + +-------------------------------------------------------------------------------- +instance Writable Biblio where + -- Shouldn't be written. + write _ _ = return () + + +-------------------------------------------------------------------------------- +biblioCompiler :: Compiler (Item Biblio) +biblioCompiler = do + filePath <- toFilePath <$> getUnderlying + makeItem =<< unsafeCompiler (Biblio <$> CSL.readBiblioFile filePath) + + +-------------------------------------------------------------------------------- +readPandocBiblio :: ReaderOptions + -> Item CSL + -> Item Biblio + -> (Item String) + -> Compiler (Item Pandoc) +readPandocBiblio ropt csl biblio item = do + -- Parse CSL file, if given + style <- unsafeCompiler $ CSL.readCSLFile Nothing . toFilePath . itemIdentifier $ csl + + -- We need to know the citation keys, add then *before* actually parsing the + -- actual page. If we don't do this, pandoc won't even consider them + -- citations! + let Biblio refs = itemBody biblio + pandoc <- itemBody <$> readPandocWith ropt item + let pandoc' = processCites style refs pandoc + + return $ fmap (const pandoc') item + +-------------------------------------------------------------------------------- +pandocBiblioCompiler :: String -> String -> Compiler (Item String) +pandocBiblioCompiler cslFileName bibFileName = do + csl <- load $ fromFilePath cslFileName + bib <- load $ fromFilePath bibFileName + liftM writePandoc + (getResourceBody >>= readPandocBiblio def csl bib) diff --git a/lib/Hakyll/Web/Pandoc/Binary.hs b/lib/Hakyll/Web/Pandoc/Binary.hs new file mode 100644 index 0000000..3c5b5a3 --- /dev/null +++ b/lib/Hakyll/Web/Pandoc/Binary.hs @@ -0,0 +1,32 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} +{-# LANGUAGE DeriveGeneric #-} +module Hakyll.Web.Pandoc.Binary where + +import Data.Binary (Binary (..)) + +import qualified Text.CSL as CSL +import qualified Text.CSL.Reference as REF +import qualified Text.CSL.Style as STY +import Text.Pandoc + +-------------------------------------------------------------------------------- +-- orphans + +instance Binary Alignment +instance Binary Block +instance Binary CSL.Reference +instance Binary Citation +instance Binary CitationMode +instance Binary Format +instance Binary Inline +instance Binary ListNumberDelim +instance Binary ListNumberStyle +instance Binary MathType +instance Binary QuoteType +instance Binary REF.CLabel +instance Binary REF.CNum +instance Binary REF.Literal +instance Binary REF.RefDate +instance Binary REF.RefType +instance Binary STY.Agent +instance Binary STY.Formatted diff --git a/lib/Hakyll/Web/Pandoc/FileType.hs b/lib/Hakyll/Web/Pandoc/FileType.hs new file mode 100644 index 0000000..3636e41 --- /dev/null +++ b/lib/Hakyll/Web/Pandoc/FileType.hs @@ -0,0 +1,74 @@ +-------------------------------------------------------------------------------- +-- | A module dealing with pandoc file extensions and associated file types +module Hakyll.Web.Pandoc.FileType + ( FileType (..) + , fileType + , itemFileType + ) where + + +-------------------------------------------------------------------------------- +import System.FilePath (splitExtension) + + +-------------------------------------------------------------------------------- +import Hakyll.Core.Identifier +import Hakyll.Core.Item + + +-------------------------------------------------------------------------------- +-- | Datatype to represent the different file types Hakyll can deal with by +-- default +data FileType + = Binary + | Css + | DocBook + | Html + | LaTeX + | LiterateHaskell FileType + | Markdown + | MediaWiki + | OrgMode + | PlainText + | Rst + | Textile + deriving (Eq, Ord, Show, Read) + + +-------------------------------------------------------------------------------- +-- | Get the file type for a certain file. The type is determined by extension. +fileType :: FilePath -> FileType +fileType = uncurry fileType' . splitExtension + where + fileType' _ ".css" = Css + fileType' _ ".dbk" = DocBook + fileType' _ ".htm" = Html + fileType' _ ".html" = Html + fileType' f ".lhs" = LiterateHaskell $ case fileType f of + -- If no extension is given, default to Markdown + LiterateHaskell + Binary -> Markdown + -- Otherwise, LaTeX + LiterateHaskell or whatever the user specified + x -> x + fileType' _ ".markdown" = Markdown + fileType' _ ".mediawiki" = MediaWiki + fileType' _ ".md" = Markdown + fileType' _ ".mdn" = Markdown + fileType' _ ".mdown" = Markdown + fileType' _ ".mdwn" = Markdown + fileType' _ ".mkd" = Markdown + fileType' _ ".mkdwn" = Markdown + fileType' _ ".org" = OrgMode + fileType' _ ".page" = Markdown + fileType' _ ".rst" = Rst + fileType' _ ".tex" = LaTeX + fileType' _ ".text" = PlainText + fileType' _ ".textile" = Textile + fileType' _ ".txt" = PlainText + fileType' _ ".wiki" = MediaWiki + fileType' _ _ = Binary -- Treat unknown files as binary + + +-------------------------------------------------------------------------------- +-- | Get the file type for the current file +itemFileType :: Item a -> FileType +itemFileType = fileType . toFilePath . itemIdentifier diff --git a/lib/Hakyll/Web/Redirect.hs b/lib/Hakyll/Web/Redirect.hs new file mode 100644 index 0000000..4760cff --- /dev/null +++ b/lib/Hakyll/Web/Redirect.hs @@ -0,0 +1,87 @@ +-- | Module used for generating HTML redirect pages. This allows renaming pages +-- to avoid breaking existing links without requiring server-side support for +-- formal 301 Redirect error codes +module Hakyll.Web.Redirect + ( Redirect (..) + , createRedirects + ) where + +import Control.Applicative ((<$>)) +import Control.Monad (forM_) +import Data.Binary (Binary (..)) +import Hakyll.Core.Compiler +import Hakyll.Core.Identifier +import Hakyll.Core.Routes +import Hakyll.Core.Rules +import Hakyll.Core.Writable (Writable (..)) + +-- | This function exposes a higher-level interface compared to using the +-- 'Redirect' type manually. +-- +-- This creates, using a database mapping broken URLs to working ones, HTML +-- files which will do HTML META tag redirect pages (since, as a static site, we +-- can't use web-server-level 301 redirects, and using JS is gross). +-- +-- This is useful for sending people using old URLs to renamed versions, dealing +-- with common typos etc, and will increase site traffic. Such broken URLs can +-- be found by looking at server logs or by using Google Webmaster Tools. +-- Broken URLs must be valid Haskell strings, non-URL-escaped valid POSIX +-- filenames, and relative links, since they will be defined in a @hakyll.hs@ +-- and during generation, written to disk with the filename corresponding to the +-- broken URLs. (Target URLs can be absolute or relative, but should be +-- URL-escaped.) So broken incoming links like <http://www.gwern.net/foo/> which +-- should be <http://www.gwern.net/foobar> cannot be fixed (since you cannot +-- create a HTML file named @"foo/"@ on disk, as that would be a directory). +-- +-- An example of a valid association list would be: +-- +-- > brokenLinks = +-- > [ ("projects.html", "http://github.com/gwern") +-- > , ("/Black-market archive", "Black-market%20archives") +-- > ] +-- +-- In which case the functionality can then be used in `main` with a line like: +-- +-- > version "redirects" $ createRedirects brokenLinks +-- +-- The 'version' is recommended to separate these items from your other pages. +-- +-- The on-disk files can then be uploaded with HTML mimetypes +-- (either explicitly by generating and uploading them separately, by +-- auto-detection of the filetype, or an upload tool defaulting to HTML +-- mimetype, such as calling @s3cmd@ with @--default-mime-type=text/html@) and +-- will redirect browsers and search engines going to the old/broken URLs. +-- +-- See also <https://groups.google.com/d/msg/hakyll/sWc6zxfh-uM/fUpZPsFNDgAJ>. +createRedirects :: [(Identifier, String)] -> Rules () +createRedirects redirects = + forM_ redirects $ \(ident, to) -> + create [ident] $ do + route idRoute + compile $ makeItem $! Redirect to + +-- | This datatype can be used directly if you want a lower-level interface to +-- generate redirects. For example, if you want to redirect @foo.html@ to +-- @bar.jpg@, you can use: +-- +-- > create ["foo.html"] $ do +-- > route idRoute +-- > compile $ makeItem $ Redirect "bar.jpg" +data Redirect = Redirect + { redirectTo :: String + } deriving (Eq, Ord, Show) + +instance Binary Redirect where + put (Redirect to) = put to + get = Redirect <$> get + +instance Writable Redirect where + write path = write path . fmap redirectToHtml + +redirectToHtml :: Redirect -> String +redirectToHtml (Redirect working) = + "<!DOCTYPE html><html><head><meta charset=\"utf-8\"/><meta name=\"generator\" content=\"hakyll\"/>" ++ + "<meta http-equiv=\"refresh\" content=\"0; url=" ++ working ++ + "\"><link rel=\"canonical\" href=\"" ++ working ++ + "\"><title>Permanent Redirect</title></head><body><p>The page has moved to: <a href=\"" ++ working ++ + "\">this page</a></p></body></html>" diff --git a/lib/Hakyll/Web/Tags.hs b/lib/Hakyll/Web/Tags.hs new file mode 100644 index 0000000..88119c2 --- /dev/null +++ b/lib/Hakyll/Web/Tags.hs @@ -0,0 +1,344 @@ +-------------------------------------------------------------------------------- +-- | This module containing some specialized functions to deal with tags. It +-- assumes you follow some conventions. +-- +-- We support two types of tags: tags and categories. +-- +-- To use default tags, use 'buildTags'. Tags are placed in a comma-separated +-- metadata field like this: +-- +-- > --- +-- > 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... +-- +-- To use categories, use the 'buildCategories' function. Categories are +-- determined by the directory a page is in, for example, the post +-- +-- > posts/coding/2010-01-28-hakyll-categories.markdown +-- +-- will receive the @coding@ category. +-- +-- Advanced users may implement custom systems using 'buildTagsWith' if desired. +-- +-- In the above example, we would want to create a page which lists all pages in +-- the @coding@ category, for example, with the 'Identifier': +-- +-- > tags/coding.html +-- +-- This is where the first parameter of 'buildTags' and 'buildCategories' comes +-- in. In the above case, we used the function: +-- +-- > fromCapture "tags/*.html" :: String -> Identifier +-- +-- The 'tagsRules' function lets you generate such a page for each tag in the +-- 'Rules' monad. +{-# LANGUAGE Arrows #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +module Hakyll.Web.Tags + ( Tags (..) + , getTags + , buildTagsWith + , buildTags + , buildCategories + , tagsRules + , renderTags + , renderTagCloud + , renderTagCloudWith + , tagCloudField + , tagCloudFieldWith + , renderTagList + , tagsField + , tagsFieldWith + , categoryField + , sortTagsBy + , caseInsensitiveTags + ) where + + +-------------------------------------------------------------------------------- +import Control.Arrow ((&&&)) +import Control.Monad (foldM, forM, forM_, mplus) +import Data.Char (toLower) +import Data.List (intercalate, intersperse, + sortBy) +import qualified Data.Map as M +import Data.Maybe (catMaybes, fromMaybe) +import Data.Ord (comparing) +import qualified Data.Set as S +import System.FilePath (takeBaseName, takeDirectory) +import Text.Blaze.Html (toHtml, toValue, (!)) +import Text.Blaze.Html.Renderer.String (renderHtml) +import qualified Text.Blaze.Html5 as H +import qualified Text.Blaze.Html5.Attributes as A + + +-------------------------------------------------------------------------------- +import Hakyll.Core.Compiler +import Hakyll.Core.Dependencies +import Hakyll.Core.Identifier +import Hakyll.Core.Identifier.Pattern +import Hakyll.Core.Item +import Hakyll.Core.Metadata +import Hakyll.Core.Rules +import Hakyll.Core.Util.String +import Hakyll.Web.Html +import Hakyll.Web.Template.Context + + +-------------------------------------------------------------------------------- +-- | Data about tags +data Tags = Tags + { tagsMap :: [(String, [Identifier])] + , tagsMakeId :: String -> Identifier + , tagsDependency :: Dependency + } + + +-------------------------------------------------------------------------------- +-- | Obtain tags from a page in the default way: parse them from the @tags@ +-- metadata field. This can either be a list or a comma-separated string. +getTags :: MonadMetadata m => Identifier -> m [String] +getTags identifier = do + metadata <- getMetadata identifier + return $ fromMaybe [] $ + (lookupStringList "tags" metadata) `mplus` + (map trim . splitAll "," <$> lookupString "tags" metadata) + + +-------------------------------------------------------------------------------- +-- | Obtain categories from a page. +getCategory :: MonadMetadata m => Identifier -> m [String] +getCategory = return . return . takeBaseName . takeDirectory . toFilePath + + +-------------------------------------------------------------------------------- +-- | Higher-order function to read tags +buildTagsWith :: MonadMetadata m + => (Identifier -> m [String]) + -> Pattern + -> (String -> Identifier) + -> m Tags +buildTagsWith f pattern makeId = do + ids <- getMatches pattern + tagMap <- foldM addTags M.empty ids + let set' = S.fromList ids + return $ Tags (M.toList tagMap) makeId (PatternDependency pattern set') + where + -- Create a tag map for one page + addTags tagMap id' = do + tags <- f id' + let tagMap' = M.fromList $ zip tags $ repeat [id'] + return $ M.unionWith (++) tagMap tagMap' + + +-------------------------------------------------------------------------------- +buildTags :: MonadMetadata m => Pattern -> (String -> Identifier) -> m Tags +buildTags = buildTagsWith getTags + + +-------------------------------------------------------------------------------- +buildCategories :: MonadMetadata m => Pattern -> (String -> Identifier) + -> m Tags +buildCategories = buildTagsWith getCategory + + +-------------------------------------------------------------------------------- +tagsRules :: Tags -> (String -> Pattern -> Rules ()) -> Rules () +tagsRules tags rules = + forM_ (tagsMap tags) $ \(tag, identifiers) -> + rulesExtraDependencies [tagsDependency tags] $ + create [tagsMakeId tags tag] $ + rules tag $ fromList identifiers + + +-------------------------------------------------------------------------------- +-- | Render tags in HTML (the flexible higher-order function) +renderTags :: (String -> String -> Int -> Int -> Int -> String) + -- ^ Produce a tag item: tag, url, count, min count, max count + -> ([String] -> String) + -- ^ Join items + -> Tags + -- ^ Tag cloud renderer + -> Compiler String +renderTags makeHtml concatHtml tags = do + -- In tags' we create a list: [((tag, route), count)] + tags' <- forM (tagsMap tags) $ \(tag, ids) -> do + route' <- getRoute $ tagsMakeId tags tag + return ((tag, route'), length ids) + + -- TODO: We actually need to tell a dependency here! + + 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 + makeHtml' ((tag, url), count) = + makeHtml tag (toUrl $ fromMaybe "/" url) count min' max' + + -- Render and return the HTML + return $ concatHtml $ map makeHtml' tags' + + +-------------------------------------------------------------------------------- +-- | Render a tag cloud in HTML +renderTagCloud :: Double + -- ^ Smallest font size, in percent + -> Double + -- ^ Biggest font size, in percent + -> Tags + -- ^ Input tags + -> Compiler String + -- ^ Rendered cloud +renderTagCloud = renderTagCloudWith makeLink (intercalate " ") + where + makeLink minSize maxSize tag url count min' max' = + -- Show the relative size of one 'count' in percent + let diff = 1 + fromIntegral max' - fromIntegral min' + relative = (fromIntegral count - fromIntegral min') / diff + size = floor $ minSize + relative * (maxSize - minSize) :: Int + in renderHtml $ + H.a ! A.style (toValue $ "font-size: " ++ show size ++ "%") + ! A.href (toValue url) + $ toHtml tag + + +-------------------------------------------------------------------------------- +-- | Render a tag cloud in HTML +renderTagCloudWith :: (Double -> Double -> + String -> String -> Int -> Int -> Int -> String) + -- ^ Render a single tag link + -> ([String] -> String) + -- ^ Concatenate links + -> Double + -- ^ Smallest font size, in percent + -> Double + -- ^ Biggest font size, in percent + -> Tags + -- ^ Input tags + -> Compiler String + -- ^ Rendered cloud +renderTagCloudWith makeLink cat minSize maxSize = + renderTags (makeLink minSize maxSize) cat + + +-------------------------------------------------------------------------------- +-- | Render a tag cloud in HTML as a context +tagCloudField :: String + -- ^ Destination key + -> Double + -- ^ Smallest font size, in percent + -> Double + -- ^ Biggest font size, in percent + -> Tags + -- ^ Input tags + -> Context a + -- ^ Context +tagCloudField key minSize maxSize tags = + field key $ \_ -> renderTagCloud minSize maxSize tags + + +-------------------------------------------------------------------------------- +-- | Render a tag cloud in HTML as a context +tagCloudFieldWith :: String + -- ^ Destination key + -> (Double -> Double -> + String -> String -> Int -> Int -> Int -> String) + -- ^ Render a single tag link + -> ([String] -> String) + -- ^ Concatenate links + -> Double + -- ^ Smallest font size, in percent + -> Double + -- ^ Biggest font size, in percent + -> Tags + -- ^ Input tags + -> Context a + -- ^ Context +tagCloudFieldWith key makeLink cat minSize maxSize tags = + field key $ \_ -> renderTagCloudWith makeLink cat minSize maxSize tags + + +-------------------------------------------------------------------------------- +-- | Render a simple tag list in HTML, with the tag count next to the item +-- TODO: Maybe produce a Context here +renderTagList :: Tags -> Compiler (String) +renderTagList = renderTags makeLink (intercalate ", ") + where + makeLink tag url count _ _ = renderHtml $ + H.a ! A.href (toValue url) $ toHtml (tag ++ " (" ++ show count ++ ")") + + +-------------------------------------------------------------------------------- +-- | Render tags with links with custom functions to get tags and to +-- render links +tagsFieldWith :: (Identifier -> Compiler [String]) + -- ^ Get the tags + -> (String -> (Maybe FilePath) -> Maybe H.Html) + -- ^ Render link for one tag + -> ([H.Html] -> H.Html) + -- ^ Concatenate tag links + -> String + -- ^ Destination field + -> Tags + -- ^ Tags structure + -> Context a + -- ^ Resulting context +tagsFieldWith getTags' renderLink cat key tags = field key $ \item -> do + tags' <- getTags' $ itemIdentifier item + links <- forM tags' $ \tag -> do + route' <- getRoute $ tagsMakeId tags tag + return $ renderLink tag route' + + return $ renderHtml $ cat $ catMaybes $ links + + +-------------------------------------------------------------------------------- +-- | Render tags with links +tagsField :: String -- ^ Destination key + -> Tags -- ^ Tags + -> Context a -- ^ Context +tagsField = + tagsFieldWith getTags simpleRenderLink (mconcat . intersperse ", ") + + +-------------------------------------------------------------------------------- +-- | Render the category in a link +categoryField :: String -- ^ Destination key + -> Tags -- ^ Tags + -> Context a -- ^ Context +categoryField = + tagsFieldWith getCategory simpleRenderLink (mconcat . intersperse ", ") + + +-------------------------------------------------------------------------------- +-- | Render one tag link +simpleRenderLink :: String -> (Maybe FilePath) -> Maybe H.Html +simpleRenderLink _ Nothing = Nothing +simpleRenderLink tag (Just filePath) = + Just $ H.a ! A.href (toValue $ toUrl filePath) $ toHtml tag + + +-------------------------------------------------------------------------------- +-- | Sort tags using supplied function. First element of the tuple passed to +-- the comparing function is the actual tag name. +sortTagsBy :: ((String, [Identifier]) -> (String, [Identifier]) -> Ordering) + -> Tags -> Tags +sortTagsBy f t = t {tagsMap = sortBy f (tagsMap t)} + + +-------------------------------------------------------------------------------- +-- | Sample sorting function that compares tags case insensitively. +caseInsensitiveTags :: (String, [Identifier]) -> (String, [Identifier]) + -> Ordering +caseInsensitiveTags = comparing $ map toLower . fst diff --git a/lib/Hakyll/Web/Template.hs b/lib/Hakyll/Web/Template.hs new file mode 100644 index 0000000..2a9684b --- /dev/null +++ b/lib/Hakyll/Web/Template.hs @@ -0,0 +1,154 @@ +-- | This module provides means for reading and applying 'Template's. +-- +-- Templates are tools to convert items into a string. They are perfectly suited +-- for laying out your site. +-- +-- Let's look at an example template: +-- +-- > <html> +-- > <head> +-- > <title>My crazy homepage - $title$</title> +-- > </head> +-- > <body> +-- > <div id="header"> +-- > <h1>My crazy homepage - $title$</h1> +-- > </div> +-- > <div id="content"> +-- > $body$ +-- > </div> +-- > <div id="footer"> +-- > By reading this you agree that I now own your soul +-- > </div> +-- > </body> +-- > </html> +-- +-- As you can see, the format is very simple -- @$key$@ is used to render the +-- @$key$@ field from the page, everything else is literally copied. If you want +-- to literally insert @\"$key$\"@ into your page (for example, when you're +-- writing a Hakyll tutorial) you can use +-- +-- > <p> +-- > A literal $$key$$. +-- > </p> +-- +-- Because of it's simplicity, these templates can be used for more than HTML: +-- you could make, for example, CSS or JS templates as well. +-- +-- Apart from interpolating @$key$@s from the 'Context' you can also +-- use the following macros: +-- +-- * @$if(key)$@ +-- +-- > $if(key)$ +-- > <b> Defined </b> +-- > $else$ +-- > <b> Non-defined </b> +-- > $endif$ +-- +-- This example will print @Defined@ if @key@ is defined in the +-- context and @Non-defined@ otherwise. The @$else$@ clause is +-- optional. +-- +-- * @$for(key)$@ +-- +-- The @for@ macro is used for enumerating 'Context' elements that are +-- lists, i.e. constructed using the 'listField' function. Assume that +-- in a context we have an element @listField \"key\" c itms@. Then +-- the snippet +-- +-- > $for(key)$ +-- > $x$ +-- > $sep$, +-- > $endfor$ +-- +-- would, for each item @i@ in 'itms', lookup @$x$@ in the context @c@ +-- with item @i@, interpolate it, and join the resulting list with +-- @,@. +-- +-- Another concrete example one may consider is the following. Given the +-- context +-- +-- > listField "things" (field "thing" (return . itemBody)) +-- > (sequence [makeItem "fruits", makeItem "vegetables"]) +-- +-- and a template +-- +-- > I like +-- > $for(things)$ +-- > fresh $thing$$sep$, and +-- > $endfor$ +-- +-- the resulting page would look like +-- +-- > <p> +-- > I like +-- > +-- > fresh fruits, and +-- > +-- > fresh vegetables +-- > </p> +-- +-- The @$sep$@ part can be omitted. Usually, you can get by using the +-- 'applyListTemplate' and 'applyJoinListTemplate' functions. +-- +-- * @$partial(path)$@ +-- +-- Loads a template located in a separate file and interpolates it +-- under the current context. +-- +-- Assuming that the file @test.html@ contains +-- +-- > <b>$key$</b> +-- +-- The result of rendering +-- +-- > <p> +-- > $partial("test.html")$ +-- > </p> +-- +-- is the same as the result of rendering +-- +-- > <p> +-- > <b>$key$</b> +-- > </p> +-- +-- That is, calling @$partial$@ is equivalent to just copying and pasting +-- template code. +-- +-- In the examples above you can see that the outputs contain a lot of leftover +-- whitespace that you may wish to remove. Using @'$-'@ or @'-$'@ instead of +-- @'$'@ in a macro strips all whitespace to the left or right of that clause +-- respectively. Given the context +-- +-- > listField "counts" (field "count" (return . itemBody)) +-- > (sequence [makeItem "3", makeItem "2", makeItem "1"]) +-- +-- and a template +-- +-- > <p> +-- > $for(counts)-$ +-- > $count$ +-- > $-sep$... +-- > $-endfor$ +-- > </p> +-- +-- the resulting page would look like +-- +-- > <p> +-- > 3...2...1 +-- > </p> +-- +module Hakyll.Web.Template + ( Template + , templateBodyCompiler + , templateCompiler + , applyTemplate + , loadAndApplyTemplate + , applyAsTemplate + , readTemplate + , unsafeReadTemplateFile + ) where + + +-------------------------------------------------------------------------------- +import Hakyll.Web.Template.Internal diff --git a/lib/Hakyll/Web/Template/Context.hs b/lib/Hakyll/Web/Template/Context.hs new file mode 100644 index 0000000..b6c7994 --- /dev/null +++ b/lib/Hakyll/Web/Template/Context.hs @@ -0,0 +1,379 @@ +-------------------------------------------------------------------------------- +{-# LANGUAGE CPP #-} +{-# LANGUAGE ExistentialQuantification #-} +module Hakyll.Web.Template.Context + ( ContextField (..) + , Context (..) + , field + , boolField + , constField + , listField + , listFieldWith + , functionField + , mapContext + + , defaultContext + , bodyField + , metadataField + , urlField + , pathField + , titleField + , snippetField + , dateField + , dateFieldWith + , getItemUTC + , getItemModificationTime + , modificationTimeField + , modificationTimeFieldWith + , teaserField + , teaserFieldWithSeparator + , missingField + ) where + + +-------------------------------------------------------------------------------- +import Control.Applicative (Alternative (..)) +import Control.Monad (msum) +import Data.List (intercalate) +import Data.Time.Clock (UTCTime (..)) +import Data.Time.Format (formatTime) +import qualified Data.Time.Format as TF +import Data.Time.Locale.Compat (TimeLocale, defaultTimeLocale) +import Hakyll.Core.Compiler +import Hakyll.Core.Compiler.Internal +import Hakyll.Core.Identifier +import Hakyll.Core.Item +import Hakyll.Core.Metadata +import Hakyll.Core.Provider +import Hakyll.Core.Util.String (needlePrefix, splitAll) +import Hakyll.Web.Html +import System.FilePath (splitDirectories, takeBaseName) + + +-------------------------------------------------------------------------------- +-- | Mostly for internal usage +data ContextField + = StringField String + | forall a. ListField (Context a) [Item a] + + +-------------------------------------------------------------------------------- +-- | The 'Context' monoid. Please note that the order in which you +-- compose the items is important. For example in +-- +-- > field "A" f1 <> field "A" f2 +-- +-- the first context will overwrite the second. This is especially +-- important when something is being composed with +-- 'metadataField' (or 'defaultContext'). If you want your context to be +-- overwritten by the metadata fields, compose it from the right: +-- +-- @ +-- 'metadataField' \<\> field \"date\" fDate +-- @ +-- +newtype Context a = Context + { unContext :: String -> [String] -> Item a -> Compiler ContextField + } + + +-------------------------------------------------------------------------------- +instance Monoid (Context a) where + mempty = missingField + mappend (Context f) (Context g) = Context $ \k a i -> f k a i <|> g k a i + + +-------------------------------------------------------------------------------- +field' :: String -> (Item a -> Compiler ContextField) -> Context a +field' key value = Context $ \k _ i -> if k == key then value i else empty + + +-------------------------------------------------------------------------------- +-- | Constructs a new field in the 'Context.' +field + :: String -- ^ Key + -> (Item a -> Compiler String) -- ^ Function that constructs a value based + -- on the item + -> Context a +field key value = field' key (fmap StringField . value) + + +-------------------------------------------------------------------------------- +-- | Creates a 'field' to use with the @$if()$@ template macro. +boolField + :: String + -> (Item a -> Bool) + -> Context a +boolField name f = field name (\i -> if f i + then pure (error $ unwords ["no string value for bool field:",name]) + else empty) + + +-------------------------------------------------------------------------------- +-- | Creates a 'field' that does not depend on the 'Item' +constField :: String -> String -> Context a +constField key = field key . const . return + + +-------------------------------------------------------------------------------- +listField :: String -> Context a -> Compiler [Item a] -> Context b +listField key c xs = listFieldWith key c (const xs) + + +-------------------------------------------------------------------------------- +listFieldWith + :: String -> Context a -> (Item b -> Compiler [Item a]) -> Context b +listFieldWith key c f = field' key $ fmap (ListField c) . f + + +-------------------------------------------------------------------------------- +functionField :: String -> ([String] -> Item a -> Compiler String) -> Context a +functionField name value = Context $ \k args i -> + if k == name + then StringField <$> value args i + else empty + + +-------------------------------------------------------------------------------- +mapContext :: (String -> String) -> Context a -> Context a +mapContext f (Context c) = Context $ \k a i -> do + fld <- c k a i + case fld of + StringField str -> return $ StringField (f str) + ListField _ _ -> fail $ + "Hakyll.Web.Template.Context.mapContext: " ++ + "can't map over a ListField!" + +-------------------------------------------------------------------------------- +-- | A context that allows snippet inclusion. In processed file, use as: +-- +-- > ... +-- > $snippet("path/to/snippet/")$ +-- > ... +-- +-- The contents of the included file will not be interpolated. +-- +snippetField :: Context String +snippetField = functionField "snippet" f + where + f [contentsPath] _ = loadBody (fromFilePath contentsPath) + f _ i = error $ + "Too many arguments to function 'snippet()' in item " ++ + show (itemIdentifier i) + +-------------------------------------------------------------------------------- +-- | A context that contains (in that order) +-- +-- 1. A @$body$@ field +-- +-- 2. Metadata fields +-- +-- 3. A @$url$@ 'urlField' +-- +-- 4. A @$path$@ 'pathField' +-- +-- 5. A @$title$@ 'titleField' +defaultContext :: Context String +defaultContext = + bodyField "body" `mappend` + metadataField `mappend` + urlField "url" `mappend` + pathField "path" `mappend` + titleField "title" `mappend` + missingField + + +-------------------------------------------------------------------------------- +teaserSeparator :: String +teaserSeparator = "<!--more-->" + + +-------------------------------------------------------------------------------- +-- | Constructs a 'field' that contains the body of the item. +bodyField :: String -> Context String +bodyField key = field key $ return . itemBody + + +-------------------------------------------------------------------------------- +-- | Map any field to its metadata value, if present +metadataField :: Context a +metadataField = Context $ \k _ i -> do + value <- getMetadataField (itemIdentifier i) k + maybe empty (return . StringField) value + + +-------------------------------------------------------------------------------- +-- | Absolute url to the resulting item +urlField :: String -> Context a +urlField key = field key $ + fmap (maybe empty toUrl) . getRoute . itemIdentifier + + +-------------------------------------------------------------------------------- +-- | Filepath of the underlying file of the item +pathField :: String -> Context a +pathField key = field key $ return . toFilePath . itemIdentifier + + +-------------------------------------------------------------------------------- +-- | This title 'field' takes the basename of the underlying file by default +titleField :: String -> Context a +titleField = mapContext takeBaseName . pathField + + +-------------------------------------------------------------------------------- +-- | When the metadata has a field called @published@ in one of the +-- following formats then this function can render the date. +-- +-- * @Mon, 06 Sep 2010 00:01:00 +0000@ +-- +-- * @Mon, 06 Sep 2010 00:01:00 UTC@ +-- +-- * @Mon, 06 Sep 2010 00:01:00@ +-- +-- * @2010-09-06T00:01:00+0000@ +-- +-- * @2010-09-06T00:01:00Z@ +-- +-- * @2010-09-06T00:01:00@ +-- +-- * @2010-09-06 00:01:00+0000@ +-- +-- * @2010-09-06 00:01:00@ +-- +-- * @September 06, 2010 00:01 AM@ +-- +-- Following date-only formats are supported too (@00:00:00@ for time is +-- assumed) +-- +-- * @2010-09-06@ +-- +-- * @September 06, 2010@ +-- +-- Alternatively, when the metadata has a field called @path@ in a +-- @folder/yyyy-mm-dd-title.extension@ format (the convention for pages) +-- and no @published@ metadata field set, this function can render +-- the date. This pattern matches the file name or directory names +-- that begins with @yyyy-mm-dd@ . For example: +-- @folder//yyyy-mm-dd-title//dist//main.extension@ . +-- In case of multiple matches, the rightmost one is used. + +dateField :: String -- ^ Key in which the rendered date should be placed + -> String -- ^ Format to use on the date + -> Context a -- ^ Resulting context +dateField = dateFieldWith defaultTimeLocale + + +-------------------------------------------------------------------------------- +-- | This is an extended version of 'dateField' that allows you to +-- specify a time locale that is used for outputting the date. For more +-- details, see 'dateField'. +dateFieldWith :: TimeLocale -- ^ Output time locale + -> String -- ^ Destination key + -> String -- ^ Format to use on the date + -> Context a -- ^ Resulting context +dateFieldWith locale key format = field key $ \i -> do + time <- getItemUTC locale $ itemIdentifier i + return $ formatTime locale format time + + +-------------------------------------------------------------------------------- +-- | Parser to try to extract and parse the time from the @published@ +-- field or from the filename. See 'dateField' for more information. +-- Exported for user convenience. +getItemUTC :: MonadMetadata m + => TimeLocale -- ^ Output time locale + -> Identifier -- ^ Input page + -> m UTCTime -- ^ Parsed UTCTime +getItemUTC locale id' = do + metadata <- getMetadata id' + let tryField k fmt = lookupString k metadata >>= parseTime' fmt + paths = splitDirectories $ toFilePath id' + + maybe empty' return $ msum $ + [tryField "published" fmt | fmt <- formats] ++ + [tryField "date" fmt | fmt <- formats] ++ + [parseTime' "%Y-%m-%d" $ intercalate "-" $ take 3 $ splitAll "-" fnCand | fnCand <- reverse paths] + where + empty' = fail $ "Hakyll.Web.Template.Context.getItemUTC: " ++ + "could not parse time for " ++ show id' + parseTime' = parseTimeM True locale + formats = + [ "%a, %d %b %Y %H:%M:%S %Z" + , "%Y-%m-%dT%H:%M:%S%Z" + , "%Y-%m-%d %H:%M:%S%Z" + , "%Y-%m-%d" + , "%B %e, %Y %l:%M %p" + , "%B %e, %Y" + , "%b %d, %Y" + ] + + +-------------------------------------------------------------------------------- +-- | Get the time on which the actual file was last modified. This only works if +-- there actually is an underlying file, of couse. +getItemModificationTime + :: Identifier + -> Compiler UTCTime +getItemModificationTime identifier = do + provider <- compilerProvider <$> compilerAsk + return $ resourceModificationTime provider identifier + + +-------------------------------------------------------------------------------- +modificationTimeField :: String -- ^ Key + -> String -- ^ Format + -> Context a -- ^ Resuting context +modificationTimeField = modificationTimeFieldWith defaultTimeLocale + + +-------------------------------------------------------------------------------- +modificationTimeFieldWith :: TimeLocale -- ^ Time output locale + -> String -- ^ Key + -> String -- ^ Format + -> Context a -- ^ Resulting context +modificationTimeFieldWith locale key fmt = field key $ \i -> do + mtime <- getItemModificationTime $ itemIdentifier i + return $ formatTime locale fmt mtime + + +-------------------------------------------------------------------------------- +-- | A context with "teaser" key which contain a teaser of the item. +-- The item is loaded from the given snapshot (which should be saved +-- in the user code before any templates are applied). +teaserField :: String -- ^ Key to use + -> Snapshot -- ^ Snapshot to load + -> Context String -- ^ Resulting context +teaserField = teaserFieldWithSeparator teaserSeparator + + +-------------------------------------------------------------------------------- +-- | A context with "teaser" key which contain a teaser of the item, defined as +-- the snapshot content before the teaser separator. The item is loaded from the +-- given snapshot (which should be saved in the user code before any templates +-- are applied). +teaserFieldWithSeparator :: String -- ^ Separator to use + -> String -- ^ Key to use + -> Snapshot -- ^ Snapshot to load + -> Context String -- ^ Resulting context +teaserFieldWithSeparator separator key snapshot = field key $ \item -> do + body <- itemBody <$> loadSnapshot (itemIdentifier item) snapshot + case needlePrefix separator body of + Nothing -> fail $ + "Hakyll.Web.Template.Context: no teaser defined for " ++ + show (itemIdentifier item) + Just t -> return t + + +-------------------------------------------------------------------------------- +missingField :: Context a +missingField = Context $ \k _ i -> fail $ + "Missing field $" ++ k ++ "$ in context for item " ++ + show (itemIdentifier i) + +parseTimeM :: Bool -> TimeLocale -> String -> String -> Maybe UTCTime +#if MIN_VERSION_time(1,5,0) +parseTimeM = TF.parseTimeM +#else +parseTimeM _ = TF.parseTime +#endif diff --git a/lib/Hakyll/Web/Template/Internal.hs b/lib/Hakyll/Web/Template/Internal.hs new file mode 100644 index 0000000..d0e4d47 --- /dev/null +++ b/lib/Hakyll/Web/Template/Internal.hs @@ -0,0 +1,203 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE ScopedTypeVariables #-} +module Hakyll.Web.Template.Internal + ( Template (..) + , template + , templateBodyCompiler + , templateCompiler + , applyTemplate + , applyTemplate' + , loadAndApplyTemplate + , applyAsTemplate + , readTemplate + , unsafeReadTemplateFile + + , module Hakyll.Web.Template.Internal.Element + , module Hakyll.Web.Template.Internal.Trim + ) where + + +-------------------------------------------------------------------------------- +import Control.Monad.Except (MonadError (..)) +import Data.Binary (Binary) +import Data.List (intercalate) +import Data.Typeable (Typeable) +import GHC.Exts (IsString (..)) +import Prelude hiding (id) + + +-------------------------------------------------------------------------------- +import Hakyll.Core.Compiler +import Hakyll.Core.Identifier +import Hakyll.Core.Item +import Hakyll.Core.Writable +import Hakyll.Web.Template.Context +import Hakyll.Web.Template.Internal.Element +import Hakyll.Web.Template.Internal.Trim + + +-------------------------------------------------------------------------------- +-- | Datatype used for template substitutions. +newtype Template = Template + { unTemplate :: [TemplateElement] + } deriving (Show, Eq, Binary, Typeable) + + +-------------------------------------------------------------------------------- +instance Writable Template where + -- Writing a template is impossible + write _ _ = return () + + +-------------------------------------------------------------------------------- +instance IsString Template where + fromString = readTemplate + + +-------------------------------------------------------------------------------- +-- | Wrap the constructor to ensure trim is called. +template :: [TemplateElement] -> Template +template = Template . trim + + +-------------------------------------------------------------------------------- +readTemplate :: String -> Template +readTemplate = Template . trim . readTemplateElems + +-------------------------------------------------------------------------------- +-- | Read a template, without metadata header +templateBodyCompiler :: Compiler (Item Template) +templateBodyCompiler = cached "Hakyll.Web.Template.templateBodyCompiler" $ do + item <- getResourceBody + file <- getResourceFilePath + return $ fmap (template . readTemplateElemsFile file) item + +-------------------------------------------------------------------------------- +-- | Read complete file contents as a template +templateCompiler :: Compiler (Item Template) +templateCompiler = cached "Hakyll.Web.Template.templateCompiler" $ do + item <- getResourceString + file <- getResourceFilePath + return $ fmap (template . readTemplateElemsFile file) item + + +-------------------------------------------------------------------------------- +applyTemplate :: Template -- ^ Template + -> Context a -- ^ Context + -> Item a -- ^ Page + -> Compiler (Item String) -- ^ Resulting item +applyTemplate tpl context item = do + body <- applyTemplate' (unTemplate tpl) context item + return $ itemSetBody body item + + +-------------------------------------------------------------------------------- +applyTemplate' + :: forall a. + [TemplateElement] -- ^ Unwrapped Template + -> Context a -- ^ Context + -> Item a -- ^ Page + -> Compiler String -- ^ Resulting item +applyTemplate' tes context x = go tes + where + context' :: String -> [String] -> Item a -> Compiler ContextField + context' = unContext (context `mappend` missingField) + + go = fmap concat . mapM applyElem + + trimError = error $ "Hakyll.Web.Template.applyTemplate: template not " ++ + "fully trimmed." + + --------------------------------------------------------------------------- + + applyElem :: TemplateElement -> Compiler String + + applyElem TrimL = trimError + + applyElem TrimR = trimError + + applyElem (Chunk c) = return c + + applyElem (Expr e) = applyExpr e >>= getString e + + applyElem Escaped = return "$" + + applyElem (If e t mf) = (applyExpr e >> go t) `catchError` handler + where + handler _ = case mf of + Nothing -> return "" + Just f -> go f + + applyElem (For e b s) = applyExpr e >>= \cf -> case cf of + StringField _ -> fail $ + "Hakyll.Web.Template.applyTemplateWith: expected ListField but " ++ + "got StringField for expr " ++ show e + ListField c xs -> do + sep <- maybe (return "") go s + bs <- mapM (applyTemplate' b c) xs + return $ intercalate sep bs + + applyElem (Partial e) = do + p <- applyExpr e >>= getString e + Template tpl' <- loadBody (fromFilePath p) + applyTemplate' tpl' context x + + --------------------------------------------------------------------------- + + applyExpr :: TemplateExpr -> Compiler ContextField + + applyExpr (Ident (TemplateKey k)) = context' k [] x + + applyExpr (Call (TemplateKey k) args) = do + args' <- mapM (\e -> applyExpr e >>= getString e) args + context' k args' x + + applyExpr (StringLiteral s) = return (StringField s) + + ---------------------------------------------------------------------------- + + getString _ (StringField s) = return s + getString e (ListField _ _) = fail $ + "Hakyll.Web.Template.applyTemplateWith: expected StringField but " ++ + "got ListField for expr " ++ show e + + +-------------------------------------------------------------------------------- +-- | The following pattern is so common: +-- +-- > tpl <- loadBody "templates/foo.html" +-- > someCompiler +-- > >>= applyTemplate tpl context +-- +-- That we have a single function which does this: +-- +-- > someCompiler +-- > >>= loadAndApplyTemplate "templates/foo.html" context +loadAndApplyTemplate :: Identifier -- ^ Template identifier + -> Context a -- ^ Context + -> Item a -- ^ Page + -> Compiler (Item String) -- ^ Resulting item +loadAndApplyTemplate identifier context item = do + tpl <- loadBody identifier + applyTemplate tpl context item + + +-------------------------------------------------------------------------------- +-- | It is also possible that you want to substitute @$key$@s within the body of +-- an item. This function does that by interpreting the item body as a template, +-- and then applying it to itself. +applyAsTemplate :: Context String -- ^ Context + -> Item String -- ^ Item and template + -> Compiler (Item String) -- ^ Resulting item +applyAsTemplate context item = + let tpl = template $ readTemplateElemsFile file (itemBody item) + file = toFilePath $ itemIdentifier item + in applyTemplate tpl context item + + +-------------------------------------------------------------------------------- +unsafeReadTemplateFile :: FilePath -> Compiler Template +unsafeReadTemplateFile file = do + tpl <- unsafeCompiler $ readFile file + pure $ template $ readTemplateElemsFile file tpl + diff --git a/lib/Hakyll/Web/Template/Internal/Element.hs b/lib/Hakyll/Web/Template/Internal/Element.hs new file mode 100644 index 0000000..f564355 --- /dev/null +++ b/lib/Hakyll/Web/Template/Internal/Element.hs @@ -0,0 +1,298 @@ +-------------------------------------------------------------------------------- +-- | Module containing the elements used in a template. A template is generally +-- just a list of these elements. +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +module Hakyll.Web.Template.Internal.Element + ( TemplateKey (..) + , TemplateExpr (..) + , TemplateElement (..) + , templateElems + , readTemplateElems + , readTemplateElemsFile + ) where + + +-------------------------------------------------------------------------------- +import Control.Applicative ((<|>)) +import Control.Monad (void) +import Data.Binary (Binary, get, getWord8, put, putWord8) +import Data.List (intercalate) +import Data.Maybe (isJust) +import Data.Typeable (Typeable) +import GHC.Exts (IsString (..)) +import qualified Text.Parsec as P +import qualified Text.Parsec.String as P + + +-------------------------------------------------------------------------------- +import Hakyll.Core.Util.Parser + + +-------------------------------------------------------------------------------- +newtype TemplateKey = TemplateKey String + deriving (Binary, Show, Eq, Typeable) + + +-------------------------------------------------------------------------------- +instance IsString TemplateKey where + fromString = TemplateKey + + +-------------------------------------------------------------------------------- +-- | Elements of a template. +data TemplateElement + = Chunk String + | Expr TemplateExpr + | Escaped + -- expr, then, else + | If TemplateExpr [TemplateElement] (Maybe [TemplateElement]) + -- expr, body, separator + | For TemplateExpr [TemplateElement] (Maybe [TemplateElement]) + -- filename + | Partial TemplateExpr + | TrimL + | TrimR + deriving (Show, Eq, Typeable) + + +-------------------------------------------------------------------------------- +instance Binary TemplateElement where + put (Chunk string) = putWord8 0 >> put string + put (Expr e) = putWord8 1 >> put e + put Escaped = putWord8 2 + put (If e t f) = putWord8 3 >> put e >> put t >> put f + put (For e b s) = putWord8 4 >> put e >> put b >> put s + put (Partial e) = putWord8 5 >> put e + put TrimL = putWord8 6 + put TrimR = putWord8 7 + + get = getWord8 >>= \tag -> case tag of + 0 -> Chunk <$> get + 1 -> Expr <$> get + 2 -> pure Escaped + 3 -> If <$> get <*> get <*> get + 4 -> For <$> get <*> get <*> get + 5 -> Partial <$> get + 6 -> pure TrimL + 7 -> pure TrimR + _ -> error "Hakyll.Web.Template.Internal: Error reading cached template" + + +-------------------------------------------------------------------------------- +-- | Expression in a template +data TemplateExpr + = Ident TemplateKey + | Call TemplateKey [TemplateExpr] + | StringLiteral String + deriving (Eq, Typeable) + + +-------------------------------------------------------------------------------- +instance Show TemplateExpr where + show (Ident (TemplateKey k)) = k + show (Call (TemplateKey k) as) = + k ++ "(" ++ intercalate ", " (map show as) ++ ")" + show (StringLiteral s) = show s + + +-------------------------------------------------------------------------------- +instance Binary TemplateExpr where + put (Ident k) = putWord8 0 >> put k + put (Call k as) = putWord8 1 >> put k >> put as + put (StringLiteral s) = putWord8 2 >> put s + + get = getWord8 >>= \tag -> case tag of + 0 -> Ident <$> get + 1 -> Call <$> get <*> get + 2 -> StringLiteral <$> get + _ -> error "Hakyll.Web.Template.Internal: Error reading cached template" + + +-------------------------------------------------------------------------------- +readTemplateElems :: String -> [TemplateElement] +readTemplateElems = readTemplateElemsFile "{literal}" + + +-------------------------------------------------------------------------------- +readTemplateElemsFile :: FilePath -> String -> [TemplateElement] +readTemplateElemsFile file input = case P.parse templateElems file input of + Left err -> error $ "Cannot parse template: " ++ show err + Right t -> t + + +-------------------------------------------------------------------------------- +templateElems :: P.Parser [TemplateElement] +templateElems = mconcat <$> P.many (P.choice [ lift chunk + , lift escaped + , conditional + , for + , partial + , expr + ]) + where lift = fmap (:[]) + + +-------------------------------------------------------------------------------- +chunk :: P.Parser TemplateElement +chunk = Chunk <$> P.many1 (P.noneOf "$") + + +-------------------------------------------------------------------------------- +expr :: P.Parser [TemplateElement] +expr = P.try $ do + trimLExpr <- trimOpen + e <- expr' + trimRExpr <- trimClose + return $ [TrimL | trimLExpr] ++ [Expr e] ++ [TrimR | trimRExpr] + + +-------------------------------------------------------------------------------- +expr' :: P.Parser TemplateExpr +expr' = stringLiteral <|> call <|> ident + + +-------------------------------------------------------------------------------- +escaped :: P.Parser TemplateElement +escaped = Escaped <$ P.try (P.string "$$") + + +-------------------------------------------------------------------------------- +trimOpen :: P.Parser Bool +trimOpen = do + void $ P.char '$' + trimLIf <- P.optionMaybe $ P.try (P.char '-') + pure $ isJust trimLIf + + +-------------------------------------------------------------------------------- +trimClose :: P.Parser Bool +trimClose = do + trimIfR <- P.optionMaybe $ P.try (P.char '-') + void $ P.char '$' + pure $ isJust trimIfR + + +-------------------------------------------------------------------------------- +conditional :: P.Parser [TemplateElement] +conditional = P.try $ do + -- if + trimLIf <- trimOpen + void $ P.string "if(" + e <- expr' + void $ P.char ')' + trimRIf <- trimClose + -- then + thenBranch <- templateElems + -- else + elseParse <- opt "else" + -- endif + trimLEnd <- trimOpen + void $ P.string "endif" + trimREnd <- trimClose + + -- As else is optional we need to sort out where any Trim_s need to go. + let (thenBody, elseBody) = maybe (thenNoElse, Nothing) thenElse elseParse + where thenNoElse = + [TrimR | trimRIf] ++ thenBranch ++ [TrimL | trimLEnd] + + thenElse (trimLElse, elseBranch, trimRElse) = (thenB, elseB) + where thenB = [TrimR | trimRIf] + ++ thenBranch + ++ [TrimL | trimLElse] + + elseB = Just $ [TrimR | trimRElse] + ++ elseBranch + ++ [TrimL | trimLEnd] + + pure $ [TrimL | trimLIf] ++ [If e thenBody elseBody] ++ [TrimR | trimREnd] + + +-------------------------------------------------------------------------------- +for :: P.Parser [TemplateElement] +for = P.try $ do + -- for + trimLFor <- trimOpen + void $ P.string "for(" + e <- expr' + void $ P.char ')' + trimRFor <- trimClose + -- body + bodyBranch <- templateElems + -- sep + sepParse <- opt "sep" + -- endfor + trimLEnd <- trimOpen + void $ P.string "endfor" + trimREnd <- trimClose + + -- As sep is optional we need to sort out where any Trim_s need to go. + let (forBody, sepBody) = maybe (forNoSep, Nothing) forSep sepParse + where forNoSep = + [TrimR | trimRFor] ++ bodyBranch ++ [TrimL | trimLEnd] + + forSep (trimLSep, sepBranch, trimRSep) = (forB, sepB) + where forB = [TrimR | trimRFor] + ++ bodyBranch + ++ [TrimL | trimLSep] + + sepB = Just $ [TrimR | trimRSep] + ++ sepBranch + ++ [TrimL | trimLEnd] + + pure $ [TrimL | trimLFor] ++ [For e forBody sepBody] ++ [TrimR | trimREnd] + + +-------------------------------------------------------------------------------- +partial :: P.Parser [TemplateElement] +partial = P.try $ do + trimLPart <- trimOpen + void $ P.string "partial(" + e <- expr' + void $ P.char ')' + trimRPart <- trimClose + + pure $ [TrimL | trimLPart] ++ [Partial e] ++ [TrimR | trimRPart] + + +-------------------------------------------------------------------------------- +ident :: P.Parser TemplateExpr +ident = P.try $ Ident <$> key + + +-------------------------------------------------------------------------------- +call :: P.Parser TemplateExpr +call = P.try $ do + f <- key + void $ P.char '(' + P.spaces + as <- P.sepBy expr' (P.spaces >> P.char ',' >> P.spaces) + P.spaces + void $ P.char ')' + return $ Call f as + + +-------------------------------------------------------------------------------- +stringLiteral :: P.Parser TemplateExpr +stringLiteral = do + void $ P.char '\"' + str <- P.many $ do + x <- P.noneOf "\"" + if x == '\\' then P.anyChar else return x + void $ P.char '\"' + return $ StringLiteral str + + +-------------------------------------------------------------------------------- +key :: P.Parser TemplateKey +key = TemplateKey <$> metadataKey + + +-------------------------------------------------------------------------------- +opt :: String -> P.Parser (Maybe (Bool, [TemplateElement], Bool)) +opt clause = P.optionMaybe $ P.try $ do + trimL <- trimOpen + void $ P.string clause + trimR <- trimClose + branch <- templateElems + pure (trimL, branch, trimR) + diff --git a/lib/Hakyll/Web/Template/Internal/Trim.hs b/lib/Hakyll/Web/Template/Internal/Trim.hs new file mode 100644 index 0000000..e416ff2 --- /dev/null +++ b/lib/Hakyll/Web/Template/Internal/Trim.hs @@ -0,0 +1,95 @@ +-------------------------------------------------------------------------------- +-- | Module for trimming whitespace from tempaltes. +module Hakyll.Web.Template.Internal.Trim + ( trim + ) where + + +-------------------------------------------------------------------------------- +import Data.Char (isSpace) +import Data.List (dropWhileEnd) + + +-------------------------------------------------------------------------------- +import Hakyll.Web.Template.Internal.Element + + +-------------------------------------------------------------------------------- +trim :: [TemplateElement] -> [TemplateElement] +trim = cleanse . canonicalize + + +-------------------------------------------------------------------------------- +-- | Apply the Trim nodes to the Chunks. +cleanse :: [TemplateElement] -> [TemplateElement] +cleanse = recurse cleanse . process + where process [] = [] + process (TrimR:Chunk str:ts) = let str' = dropWhile isSpace str + in if null str' + then process ts + -- Might need to TrimL. + else process $ Chunk str':ts + + process (Chunk str:TrimL:ts) = let str' = dropWhileEnd isSpace str + in if null str' + then process ts + else Chunk str':process ts + + process (t:ts) = t:process ts + +-------------------------------------------------------------------------------- +-- | Enforce the invariant that: +-- +-- * Every 'TrimL' has a 'Chunk' to its left. +-- * Every 'TrimR' has a 'Chunk' to its right. +-- +canonicalize :: [TemplateElement] -> [TemplateElement] +canonicalize = go + where go t = let t' = redundant . swap $ dedupe t + in if t == t' then t else go t' + + +-------------------------------------------------------------------------------- +-- | Remove the 'TrimR' and 'TrimL's that are no-ops. +redundant :: [TemplateElement] -> [TemplateElement] +redundant = recurse redundant . process + where -- Remove the leading 'TrimL's. + process (TrimL:ts) = process ts + -- Remove trailing 'TrimR's. + process ts = foldr trailing [] ts + where trailing TrimR [] = [] + trailing x xs = x:xs + + +-------------------------------------------------------------------------------- +-- >>> swap $ [TrimR, TrimL] +-- [TrimL, TrimR] +swap :: [TemplateElement] -> [TemplateElement] +swap = recurse swap . process + where process [] = [] + process (TrimR:TrimL:ts) = TrimL:process (TrimR:ts) + process (t:ts) = t:process ts + + +-------------------------------------------------------------------------------- +-- | Remove 'TrimR' and 'TrimL' duplication. +dedupe :: [TemplateElement] -> [TemplateElement] +dedupe = recurse dedupe . process + where process [] = [] + process (TrimR:TrimR:ts) = process (TrimR:ts) + process (TrimL:TrimL:ts) = process (TrimL:ts) + process (t:ts) = t:process ts + + +-------------------------------------------------------------------------------- +-- | @'recurse' f t@ applies f to every '[TemplateElement]' in t. +recurse :: ([TemplateElement] -> [TemplateElement]) + -> [TemplateElement] + -> [TemplateElement] +recurse _ [] = [] +recurse f (x:xs) = process x:recurse f xs + where process y = case y of + If e tb eb -> If e (f tb) (f <$> eb) + For e t s -> For e (f t) (f <$> s) + _ -> y + diff --git a/lib/Hakyll/Web/Template/List.hs b/lib/Hakyll/Web/Template/List.hs new file mode 100644 index 0000000..4d769fc --- /dev/null +++ b/lib/Hakyll/Web/Template/List.hs @@ -0,0 +1,91 @@ +-------------------------------------------------------------------------------- +-- | Provides an easy way to combine several items in a list. The applications +-- are obvious: +-- +-- * A post list on a blog +-- +-- * An image list in a gallery +-- +-- * A sitemap +{-# LANGUAGE TupleSections #-} +module Hakyll.Web.Template.List + ( applyTemplateList + , applyJoinTemplateList + , chronological + , recentFirst + , sortChronological + , sortRecentFirst + ) where + + +-------------------------------------------------------------------------------- +import Control.Monad (liftM) +import Data.List (intersperse, sortBy) +import Data.Ord (comparing) +import Data.Time.Locale.Compat (defaultTimeLocale) + + +-------------------------------------------------------------------------------- +import Hakyll.Core.Compiler +import Hakyll.Core.Identifier +import Hakyll.Core.Item +import Hakyll.Core.Metadata +import Hakyll.Web.Template +import Hakyll.Web.Template.Context + + +-------------------------------------------------------------------------------- +-- | Generate a string of a listing of pages, after applying a template to each +-- page. +applyTemplateList :: Template + -> Context a + -> [Item a] + -> Compiler String +applyTemplateList = applyJoinTemplateList "" + + +-------------------------------------------------------------------------------- +-- | Join a listing of pages with a string in between, after applying a template +-- to each page. +applyJoinTemplateList :: String + -> Template + -> Context a + -> [Item a] + -> Compiler String +applyJoinTemplateList delimiter tpl context items = do + items' <- mapM (applyTemplate tpl context) items + return $ concat $ intersperse delimiter $ map itemBody items' + + +-------------------------------------------------------------------------------- +-- | Sort pages chronologically. Uses the same method as 'dateField' for +-- extracting the date. +chronological :: MonadMetadata m => [Item a] -> m [Item a] +chronological = + sortByM $ getItemUTC defaultTimeLocale . 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 + + +-------------------------------------------------------------------------------- +-- | The reverse of 'chronological' +recentFirst :: MonadMetadata m => [Item a] -> m [Item a] +recentFirst = liftM reverse . chronological + + +-------------------------------------------------------------------------------- +-- | Version of 'chronological' which doesn't need the actual items. +sortChronological + :: MonadMetadata m => [Identifier] -> m [Identifier] +sortChronological ids = + liftM (map itemIdentifier) $ chronological [Item i () | i <- ids] + + +-------------------------------------------------------------------------------- +-- | Version of 'recentFirst' which doesn't need the actual items. +sortRecentFirst + :: MonadMetadata m => [Identifier] -> m [Identifier] +sortRecentFirst ids = + liftM (map itemIdentifier) $ recentFirst [Item i () | i <- ids] |