summaryrefslogtreecommitdiff
path: root/src/Hakyll/Web
diff options
context:
space:
mode:
Diffstat (limited to 'src/Hakyll/Web')
-rw-r--r--src/Hakyll/Web/CompressCss.hs51
-rw-r--r--src/Hakyll/Web/Feed.hs124
-rw-r--r--src/Hakyll/Web/FileType.hs55
-rw-r--r--src/Hakyll/Web/Page.hs124
-rw-r--r--src/Hakyll/Web/Page/Internal.hs50
-rw-r--r--src/Hakyll/Web/Page/Metadata.hs131
-rw-r--r--src/Hakyll/Web/Page/Read.hs60
-rw-r--r--src/Hakyll/Web/Pandoc.hs110
-rw-r--r--src/Hakyll/Web/Preview/Server.hs72
-rw-r--r--src/Hakyll/Web/RelativizeUrls.hs62
-rw-r--r--src/Hakyll/Web/Tags.hs180
-rw-r--r--src/Hakyll/Web/Template.hs109
-rw-r--r--src/Hakyll/Web/Template/Internal.hs45
-rw-r--r--src/Hakyll/Web/Template/Read.hs10
-rw-r--r--src/Hakyll/Web/Template/Read/Hakyll.hs35
-rw-r--r--src/Hakyll/Web/Template/Read/Hamlet.hs46
-rw-r--r--src/Hakyll/Web/Util/Url.hs30
17 files changed, 1294 insertions, 0 deletions
diff --git a/src/Hakyll/Web/CompressCss.hs b/src/Hakyll/Web/CompressCss.hs
new file mode 100644
index 0000000..2df08fd
--- /dev/null
+++ b/src/Hakyll/Web/CompressCss.hs
@@ -0,0 +1,51 @@
+-- | 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.Char (isSpace)
+import Data.List (isPrefixOf)
+import Control.Arrow ((>>^))
+
+import Hakyll.Core.Compiler
+import Hakyll.Core.ResourceProvider
+import Hakyll.Core.Util.String
+
+-- | Compiler form of 'compressCss'
+--
+compressCssCompiler :: Compiler Resource String
+compressCssCompiler = getResourceString >>^ compressCss
+
+-- | Compress CSS to speed up your site.
+--
+compressCss :: String -> String
+compressCss = compressSeparators
+ . stripComments
+ . compressWhitespace
+
+-- | Compresses certain forms of separators.
+--
+compressSeparators :: String -> String
+compressSeparators = replaceAll "; *}" (const "}")
+ . replaceAll " *([{};:]) *" (take 1 . dropWhile isSpace)
+ . replaceAll ";;*" (const ";")
+
+-- | Compresses all whitespace.
+--
+compressWhitespace :: String -> String
+compressWhitespace = replaceAll "[ \t\n][ \t\n]*" (const " ")
+
+-- | Function that strips CSS comments away.
+--
+stripComments :: String -> String
+stripComments [] = []
+stripComments str
+ | isPrefixOf "/*" str = stripComments $ eatComments $ drop 2 str
+ | otherwise = head str : stripComments (drop 1 str)
+ where
+ eatComments str' | null str' = []
+ | isPrefixOf "*/" str' = drop 2 str'
+ | otherwise = eatComments $ drop 1 str'
diff --git a/src/Hakyll/Web/Feed.hs b/src/Hakyll/Web/Feed.hs
new file mode 100644
index 0000000..85674c6
--- /dev/null
+++ b/src/Hakyll/Web/Feed.hs
@@ -0,0 +1,124 @@
+-- | 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 pages 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.Page.Metadata.renderDateField'.
+--
+module Hakyll.Web.Feed
+ ( FeedConfiguration (..)
+ , renderRss
+ , renderAtom
+ ) where
+
+import Prelude hiding (id)
+import Control.Category (id)
+import Control.Arrow ((>>>), arr, (&&&))
+import Control.Monad ((<=<))
+import Data.Maybe (fromMaybe, listToMaybe)
+
+import Hakyll.Core.Compiler
+import Hakyll.Web.Page
+import Hakyll.Web.Page.Metadata
+import Hakyll.Web.Template
+import Hakyll.Web.Template.Read.Hakyll (readTemplate)
+import Hakyll.Web.Util.Url
+
+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
+ , -- | Absolute root URL of the feed site (e.g. @http://jaspervdj.be@)
+ feedRoot :: String
+ }
+
+-- | This is an auxiliary function to create a listing that is, in fact, a feed.
+-- The items should be sorted on date. The @$timestamp@ field should be set.
+--
+createFeed :: Template -- ^ Feed template
+ -> Template -- ^ Item template
+ -> String -- ^ URL of the feed
+ -> FeedConfiguration -- ^ Feed configuration
+ -> [Page String] -- ^ Items to include
+ -> String -- ^ Resulting feed
+createFeed feedTemplate itemTemplate url configuration items =
+ pageBody $ applyTemplate feedTemplate
+ $ setField "timestamp" timestamp
+ $ setField "title" (feedTitle configuration)
+ $ setField "description" (feedDescription configuration)
+ $ setField "authorName" (feedDescription configuration)
+ $ setField "root" (feedRoot configuration)
+ $ setField "url" url
+ $ fromBody body
+ where
+ -- Preprocess items
+ items' = flip map items $ applyTemplate itemTemplate
+ . setField "root" (feedRoot configuration)
+
+ -- Body: concatenated items
+ body = concat $ map pageBody items'
+
+ -- Take the first timestamp, which should be the most recent
+ timestamp = fromMaybe "Unknown" $ do
+ p <- listToMaybe items
+ return $ getField "timestamp" p
+
+
+-- | Abstract function to render any feed.
+--
+renderFeed :: FilePath -- ^ Feed template
+ -> FilePath -- ^ Item template
+ -> FeedConfiguration -- ^ Feed configuration
+ -> Compiler [Page String] String -- ^ Feed compiler
+renderFeed feedTemplate itemTemplate configuration =
+ id &&& getRoute >>> renderFeed'
+ where
+ -- Arrow rendering the feed from the items and the URL
+ renderFeed' = unsafeCompiler $ \(items, url) -> do
+ feedTemplate' <- loadTemplate feedTemplate
+ itemTemplate' <- loadTemplate itemTemplate
+ let url' = toUrl $ fromMaybe noUrl url
+ return $ createFeed feedTemplate' itemTemplate' url' configuration items
+
+ -- Auxiliary: load a template from a datafile
+ loadTemplate = fmap readTemplate . readFile <=< getDataFileName
+
+ -- URL is required to have a valid field
+ noUrl = error "Hakyll.Web.Feed.renderFeed: no route specified"
+
+-- | Render an RSS feed with a number of items.
+--
+renderRss :: FeedConfiguration -- ^ Feed configuration
+ -> Compiler [Page String] String -- ^ Feed compiler
+renderRss configuration = arr (map renderDate)
+ >>> renderFeed "templates/rss.xml" "templates/rss-item.xml" configuration
+ where
+ renderDate = renderDateField "timestamp" "%a, %d %b %Y %H:%M:%S UT"
+ "No date found."
+
+-- | Render an Atom feed with a number of items.
+--
+renderAtom :: FeedConfiguration -- ^ Feed configuration
+ -> Compiler [Page String] String -- ^ Feed compiler
+renderAtom configuration = arr (map renderDate)
+ >>> renderFeed "templates/atom.xml" "templates/atom-item.xml" configuration
+ where
+ renderDate = renderDateField "timestamp" "%Y-%m-%dT%H:%M:%SZ"
+ "No date found."
diff --git a/src/Hakyll/Web/FileType.hs b/src/Hakyll/Web/FileType.hs
new file mode 100644
index 0000000..cd1188a
--- /dev/null
+++ b/src/Hakyll/Web/FileType.hs
@@ -0,0 +1,55 @@
+-- | A module dealing with common file extensions and associated file types.
+--
+module Hakyll.Web.FileType
+ ( FileType (..)
+ , fileType
+ , getFileType
+ ) where
+
+import System.FilePath (takeExtension)
+import Control.Arrow ((>>^))
+
+import Hakyll.Core.Identifier
+import Hakyll.Core.Compiler
+
+-- | Datatype to represent the different file types Hakyll can deal with by
+-- default
+--
+data FileType
+ = Html
+ | LaTeX
+ | LiterateHaskell FileType
+ | Markdown
+ | Rst
+ | PlainText
+ | Css
+ | Binary
+ deriving (Eq, Ord, Show, Read)
+
+-- | Get the file type for a certain file. The type is determined by extension.
+--
+fileType :: FilePath -> FileType
+fileType = fileType' . takeExtension
+ where
+ fileType' ".htm" = Html
+ fileType' ".html" = Html
+ fileType' ".lhs" = LiterateHaskell Markdown
+ fileType' ".markdown" = Markdown
+ fileType' ".md" = Markdown
+ fileType' ".mdn" = Markdown
+ fileType' ".mdown" = Markdown
+ fileType' ".mdwn" = Markdown
+ fileType' ".mkd" = Markdown
+ fileType' ".mkdwn" = Markdown
+ fileType' ".page" = Markdown
+ fileType' ".rst" = Rst
+ fileType' ".tex" = LaTeX
+ fileType' ".text" = PlainText
+ fileType' ".txt" = PlainText
+ fileType' ".css" = Css
+ fileType' _ = Binary -- Treat unknown files as binary
+
+-- | Get the file type for the current file
+--
+getFileType :: Compiler a FileType
+getFileType = getIdentifier >>^ fileType . toFilePath
diff --git a/src/Hakyll/Web/Page.hs b/src/Hakyll/Web/Page.hs
new file mode 100644
index 0000000..955e1a8
--- /dev/null
+++ b/src/Hakyll/Web/Page.hs
@@ -0,0 +1,124 @@
+-- | A page is a key-value mapping, representing a page on your site
+--
+-- A page is an important concept in Hakyll. It is a key-value mapping, and has
+-- one field with an arbitrary type. A 'Page' thus consists of
+--
+-- * a key-value mapping (of the type @Map String String@);
+--
+-- * a value (of the type @a@).
+--
+-- Usually, the value will be a 'String' as well, and the value will be the body
+-- of the page.
+--
+-- Pages can be constructed using Haskell, but they are usually parsed from a
+-- file. The file format for pages is pretty straightforward.
+--
+-- > This is a simple page
+-- > consisting of two lines.
+--
+-- This is a valid page with two lines. If we load this in Hakyll, there would
+-- be no metadata, and the body would be the given text. Let's look at a page
+-- with some metadata.
+--
+-- > ---
+-- > title: Alice's Adventures in Wonderland
+-- > author: Lewis Caroll
+-- > year: 1865
+-- > ---
+-- >
+-- > Chapter I
+-- > =========
+-- >
+-- > Down the Rabbit-Hole
+-- > --------------------
+-- >
+-- > Alice was beginning to get very tired of sitting by her sister on the bank,
+-- > and of having nothing to do: once or twice she had peeped into the book her
+-- > sister was reading, but it had no pictures or conversations in it, "and
+-- > what is the use of a book," thought Alice "without pictures or
+-- > conversation?"
+-- >
+-- > ...
+--
+-- As you can see, we construct a metadata header in Hakyll using @---@. Then,
+-- we simply list all @key: value@ pairs, and end with @---@ again. This page
+-- contains three metadata fields and a body. The body is given in markdown
+-- format, which can be easily rendered to HTML by Hakyll, using pandoc.
+--
+{-# LANGUAGE DeriveDataTypeable #-}
+module Hakyll.Web.Page
+ ( Page (..)
+ , fromBody
+ , fromMap
+ , toMap
+ , readPageCompiler
+ , pageCompiler
+ , addDefaultFields
+ , sortByBaseName
+ ) where
+
+import Prelude hiding (id)
+import Control.Category (id)
+import Control.Arrow (arr, (>>^), (&&&), (>>>))
+import System.FilePath (takeBaseName, takeDirectory)
+import qualified Data.Map as M
+import Data.List (sortBy)
+import Data.Ord (comparing)
+
+import Hakyll.Core.Identifier
+import Hakyll.Core.Compiler
+import Hakyll.Core.ResourceProvider
+import Hakyll.Web.Page.Internal
+import Hakyll.Web.Page.Read
+import Hakyll.Web.Page.Metadata
+import Hakyll.Web.Pandoc
+import Hakyll.Web.Template
+import Hakyll.Web.Util.Url
+
+-- | Create a page from a body, without metadata
+--
+fromBody :: a -> Page a
+fromBody = Page M.empty
+
+-- | Read a page (do not render it)
+--
+readPageCompiler :: Compiler Resource (Page String)
+readPageCompiler = getResourceString >>^ readPage
+
+-- | Read a page, add default fields, substitute fields and render using pandoc
+--
+pageCompiler :: Compiler Resource (Page String)
+pageCompiler = cached "Hakyll.Web.Page.pageCompiler" $
+ readPageCompiler >>> addDefaultFields >>> arr applySelf >>> pageRenderPandoc
+
+-- | Add a number of default metadata fields to a page. These fields include:
+--
+-- * @$url@
+--
+-- * @$category@
+--
+-- * @$title@
+--
+-- * @$path@
+--
+addDefaultFields :: Compiler (Page a) (Page a)
+addDefaultFields = (getRoute &&& id >>^ uncurry addRoute)
+ >>> (getIdentifier &&& id >>^ uncurry addIdentifier)
+ where
+ -- Add root and url, based on route
+ addRoute Nothing = id
+ addRoute (Just r) = setField "url" (toUrl r)
+
+ -- Add title and category, based on identifier
+ addIdentifier i = setField "title" (takeBaseName p)
+ . setField "category" (takeBaseName $ takeDirectory p)
+ . setField "path" p
+ where
+ p = toFilePath i
+
+-- | Sort posts based on the basename of the post. This is equivalent to a
+-- chronologival sort, because of the @year-month-day-title.extension@ naming
+-- convention in Hakyll.
+--
+sortByBaseName :: [Page a] -> [Page a]
+sortByBaseName = sortBy $ comparing $ takeBaseName . getField "path"
diff --git a/src/Hakyll/Web/Page/Internal.hs b/src/Hakyll/Web/Page/Internal.hs
new file mode 100644
index 0000000..55067ed
--- /dev/null
+++ b/src/Hakyll/Web/Page/Internal.hs
@@ -0,0 +1,50 @@
+-- | Internal representation of the page datatype
+--
+{-# LANGUAGE DeriveDataTypeable #-}
+module Hakyll.Web.Page.Internal
+ ( Page (..)
+ , fromMap
+ , toMap
+ ) where
+
+import Control.Applicative ((<$>), (<*>))
+import Data.Monoid (Monoid, mempty, mappend)
+
+import Data.Map (Map)
+import Data.Binary (Binary, get, put)
+import Data.Typeable (Typeable)
+import qualified Data.Map as M
+
+import Hakyll.Core.Writable
+
+-- | Type used to represent pages
+--
+data Page a = Page
+ { pageMetadata :: Map String String
+ , pageBody :: a
+ } deriving (Eq, Show, Typeable)
+
+instance Monoid a => Monoid (Page a) where
+ mempty = Page M.empty mempty
+ mappend (Page m1 b1) (Page m2 b2) =
+ Page (M.union m1 m2) (mappend b1 b2)
+
+instance Functor Page where
+ fmap f (Page m b) = Page m (f b)
+
+instance Binary a => Binary (Page a) where
+ put (Page m b) = put m >> put b
+ get = Page <$> get <*> get
+
+instance Writable a => Writable (Page a) where
+ write p (Page _ b) = write p b
+
+-- | Create a metadata page, without a body
+--
+fromMap :: Monoid a => Map String String -> Page a
+fromMap m = Page m mempty
+
+-- | Convert a page to a map. The body will be placed in the @body@ key.
+--
+toMap :: Page String -> Map String String
+toMap (Page m b) = M.insert "body" b m
diff --git a/src/Hakyll/Web/Page/Metadata.hs b/src/Hakyll/Web/Page/Metadata.hs
new file mode 100644
index 0000000..72742e6
--- /dev/null
+++ b/src/Hakyll/Web/Page/Metadata.hs
@@ -0,0 +1,131 @@
+-- | Provides various functions to manipulate the metadata fields of a page
+--
+module Hakyll.Web.Page.Metadata
+ ( getField
+ , getFieldMaybe
+ , setField
+ , setFieldA
+ , renderField
+ , changeField
+ , copyField
+ , renderDateField
+ , renderDateFieldWith
+ ) where
+
+import Prelude hiding (id)
+import Control.Category (id)
+import Control.Arrow (Arrow, (>>>), (***), arr)
+import Data.List (intercalate)
+import Data.Maybe (fromMaybe)
+import Data.Time.Clock (UTCTime)
+import Data.Time.Format (parseTime, formatTime)
+import qualified Data.Map as M
+import System.FilePath (takeFileName)
+import System.Locale (TimeLocale, defaultTimeLocale)
+
+import Hakyll.Web.Page.Internal
+import Hakyll.Core.Util.String
+
+-- | Get a metadata field. If the field does not exist, the empty string is
+-- returned.
+--
+getField :: String -- ^ Key
+ -> Page a -- ^ Page
+ -> String -- ^ Value
+getField key = fromMaybe "" . getFieldMaybe key
+
+-- | Get a field in a 'Maybe' wrapper
+--
+getFieldMaybe :: String -- ^ Key
+ -> Page a -- ^ Page
+ -> Maybe String -- ^ Value, if found
+getFieldMaybe key = M.lookup key . pageMetadata
+
+-- | Add a metadata field. If the field already exists, it is not overwritten.
+--
+setField :: String -- ^ Key
+ -> String -- ^ Value
+ -> Page a -- ^ Page to add it to
+ -> Page a -- ^ Resulting page
+setField k v (Page m b) = Page (M.insertWith (flip const) k v m) b
+
+-- | Arrow-based variant of 'setField'. Because of it's type, this function is
+-- very usable together with the different 'require' functions.
+--
+setFieldA :: Arrow a
+ => String -- ^ Key
+ -> a x String -- ^ Value arrow
+ -> a (Page b, x) (Page b) -- ^ Resulting arrow
+setFieldA k v = id *** v >>> arr (uncurry $ flip $ setField k)
+
+-- | Do something with a metadata value, but keep the old value as well. If the
+-- key given is not present in the metadata, nothing will happen. If the source
+-- and destination keys are the same, the value will be changed (but you should
+-- use 'changeField' for this purpose).
+--
+renderField :: String -- ^ Key of which the value should be copied
+ -> String -- ^ Key the value should be copied to
+ -> (String -> String) -- ^ Function to apply on the value
+ -> Page a -- ^ Page on which this should be applied
+ -> Page a -- ^ Resulting page
+renderField src dst f page = case M.lookup src (pageMetadata page) of
+ Nothing -> page
+ Just value -> setField dst (f value) page
+
+-- | Change a metadata value.
+--
+-- > import Data.Char (toUpper)
+-- > changeField "title" (map toUpper)
+--
+-- Will put the title in UPPERCASE.
+--
+changeField :: String -- ^ Key to change.
+ -> (String -> String) -- ^ Function to apply on the value.
+ -> Page a -- ^ Page to change
+ -> Page a -- ^ Resulting page
+changeField key = renderField key key
+
+-- | Make a copy of a metadata field (put the value belonging to a certain key
+-- under some other key as well)
+--
+copyField :: String -- ^ Key to copy
+ -> String -- ^ Destination to copy to
+ -> Page a -- ^ Page on which this should be applied
+ -> Page a -- ^ Resulting page
+copyField src dst = renderField src dst id
+
+-- | When the metadata has a field called @path@ in a
+-- @folder/yyyy-mm-dd-title.extension@ format (the convention for pages),
+-- this function can render the date.
+--
+-- > renderDate "date" "%B %e, %Y" "Date unknown"
+--
+-- Will render something like @January 32, 2010@.
+--
+renderDateField :: String -- ^ Key in which the rendered date should be placed
+ -> String -- ^ Format to use on the date
+ -> String -- ^ Default value, in case the date cannot be parsed
+ -> Page a -- ^ Page on which this should be applied
+ -> Page a -- ^ Resulting page
+renderDateField = renderDateFieldWith defaultTimeLocale
+
+-- | This is an extended version of 'renderDateField' that allows you to
+-- specify a time locale that is used for outputting the date. For more
+-- details, see 'renderDateField'.
+--
+renderDateFieldWith :: TimeLocale -- ^ Output time locale
+ -> String -- ^ Destination key
+ -> String -- ^ Format to use on the date
+ -> String -- ^ Default value
+ -> Page a -- ^ Target page
+ -> Page a -- ^ Resulting page
+renderDateFieldWith locale key format defaultValue =
+ renderField "path" key renderDate'
+ where
+ renderDate' filePath = fromMaybe defaultValue $ do
+ let dateString = intercalate "-" $ take 3
+ $ splitAll "-" $ takeFileName filePath
+ time <- parseTime defaultTimeLocale
+ "%Y-%m-%d"
+ dateString :: Maybe UTCTime
+ return $ formatTime locale format time
diff --git a/src/Hakyll/Web/Page/Read.hs b/src/Hakyll/Web/Page/Read.hs
new file mode 100644
index 0000000..cf39ddd
--- /dev/null
+++ b/src/Hakyll/Web/Page/Read.hs
@@ -0,0 +1,60 @@
+-- | Module providing a function to parse a page from a file
+--
+module Hakyll.Web.Page.Read
+ ( readPage
+ ) where
+
+import Control.Applicative ((<$>), (<*>))
+import Control.Arrow (second, (***))
+import Control.Monad.State (State, get, put, evalState)
+import Data.List (isPrefixOf)
+import Data.Map (Map)
+import qualified Data.Map as M
+
+import Hakyll.Web.Page.Internal
+import Hakyll.Core.Util.String
+
+-- | We're using a simple state monad as parser
+--
+type LineParser = State [String]
+
+-- | Read the metadata section from a page
+--
+parseMetadata :: LineParser (Map String String)
+parseMetadata = get >>= \content -> case content of
+ -- No lines means no metadata
+ [] -> return M.empty
+ -- Check if the file begins with a delimiter
+ (l : ls) -> if not (isPossibleDelimiter l)
+ then -- No delimiter means no metadata
+ return M.empty
+ else do -- Break the metadata section
+ let (metadata, rest) = second (drop 1) $ break (== l) ls
+ -- Put the rest back
+ put rest
+ -- Parse the metadata
+ return $ M.fromList $ map parseMetadata' metadata
+ where
+ -- Check if a line can be a delimiter
+ isPossibleDelimiter = isPrefixOf "---"
+
+ -- Parse a "key: value" string to a (key, value) tupple
+ parseMetadata' = (trim *** trim . drop 1) . break (== ':')
+
+-- | Read the body section of a page
+--
+parseBody :: LineParser String
+parseBody = do
+ body <- get
+ put []
+ return $ unlines body
+
+-- | Read an entire page
+--
+parsePage :: LineParser (Page String)
+parsePage = Page <$> parseMetadata <*> parseBody
+
+-- | Read a page from a string
+--
+readPage :: String -> Page String
+readPage = evalState parsePage . lines
diff --git a/src/Hakyll/Web/Pandoc.hs b/src/Hakyll/Web/Pandoc.hs
new file mode 100644
index 0000000..f225997
--- /dev/null
+++ b/src/Hakyll/Web/Pandoc.hs
@@ -0,0 +1,110 @@
+-- | Module exporting pandoc bindings
+--
+module Hakyll.Web.Pandoc
+ ( -- * The basic building blocks
+ readPandoc
+ , readPandocWith
+ , writePandoc
+ , writePandocWith
+
+ -- * Functions working on pages/compilers
+ , pageReadPandoc
+ , pageReadPandocWith
+ , pageRenderPandoc
+ , pageRenderPandocWith
+
+ -- * Default options
+ , defaultHakyllParserState
+ , defaultHakyllWriterOptions
+ ) where
+
+import Prelude hiding (id)
+import Control.Applicative ((<$>))
+import Control.Arrow ((>>^), (&&&))
+import Control.Category (id)
+
+import Text.Pandoc
+
+import Hakyll.Core.Compiler
+import Hakyll.Web.FileType
+import Hakyll.Web.Page.Internal
+
+-- | Read a string using pandoc, with the default options
+--
+readPandoc :: FileType -- ^ File type, determines how parsing happens
+ -> String -- ^ String to read
+ -> Pandoc -- ^ Resulting document
+readPandoc = readPandocWith defaultHakyllParserState
+
+-- | Read a string using pandoc, with the supplied options
+--
+readPandocWith :: ParserState -- ^ Parser options
+ -> FileType -- ^ File type, determines how parsing happens
+ -> String -- ^ String to read
+ -> Pandoc -- ^ Resulting document
+readPandocWith state fileType' = case fileType' of
+ Html -> readHtml state
+ LaTeX -> readLaTeX state
+ LiterateHaskell t -> readPandocWith state {stateLiterateHaskell = True} t
+ Markdown -> readMarkdown state
+ Rst -> readRST state
+ t -> error $
+ "Hakyll.Web.readPandocWith: I don't know how to read " ++ show t
+
+-- | Write a document (as HTML) using pandoc, with the default options
+--
+writePandoc :: Pandoc -- ^ Document to write
+ -> String -- ^ Resulting HTML
+writePandoc = writePandocWith defaultHakyllWriterOptions
+
+-- | Write a document (as HTML) using pandoc, with the supplied options
+--
+writePandocWith :: WriterOptions -- ^ Writer options for pandoc
+ -> Pandoc -- ^ Document to write
+ -> String -- ^ Resulting HTML
+writePandocWith = writeHtmlString
+
+-- | Read the resource using pandoc
+--
+pageReadPandoc :: Compiler (Page String) (Page Pandoc)
+pageReadPandoc = pageReadPandocWith defaultHakyllParserState
+
+-- | Read the resource using pandoc
+--
+pageReadPandocWith :: ParserState -> Compiler (Page String) (Page Pandoc)
+pageReadPandocWith state =
+ id &&& getFileType >>^ pageReadPandocWith'
+ where
+ pageReadPandocWith' (p, t) = readPandocWith state t <$> p
+
+-- | Render the resource using pandoc
+--
+pageRenderPandoc :: Compiler (Page String) (Page String)
+pageRenderPandoc =
+ pageRenderPandocWith defaultHakyllParserState defaultHakyllWriterOptions
+
+-- | Render the resource using pandoc
+--
+pageRenderPandocWith :: ParserState
+ -> WriterOptions
+ -> Compiler (Page String) (Page String)
+pageRenderPandocWith state options =
+ pageReadPandocWith state >>^ fmap (writePandocWith options)
+
+-- | The default reader options for pandoc parsing in hakyll
+--
+defaultHakyllParserState :: ParserState
+defaultHakyllParserState = defaultParserState
+ { -- The following option causes pandoc to read smart typography, a nice
+ -- and free bonus.
+ stateSmart = True
+ }
+
+-- | The default writer options for pandoc rendering in hakyll
+--
+defaultHakyllWriterOptions :: WriterOptions
+defaultHakyllWriterOptions = defaultWriterOptions
+ { -- This option causes literate haskell to be written using '>' marks in
+ -- html, which I think is a good default.
+ writerLiterateHaskell = True
+ }
diff --git a/src/Hakyll/Web/Preview/Server.hs b/src/Hakyll/Web/Preview/Server.hs
new file mode 100644
index 0000000..c550b69
--- /dev/null
+++ b/src/Hakyll/Web/Preview/Server.hs
@@ -0,0 +1,72 @@
+-- | Implements a basic static file server for previewing options
+--
+{-# LANGUAGE OverloadedStrings #-}
+module Hakyll.Web.Preview.Server
+ ( staticServer
+ ) where
+
+import Control.Monad.Trans (liftIO)
+import Control.Applicative ((<$>))
+import Codec.Binary.UTF8.String
+import System.FilePath ((</>))
+import System.Directory (doesFileExist)
+
+import qualified Data.ByteString as SB
+import Snap.Util.FileServe (serveFile)
+import Snap.Types (Snap, rqURI, getRequest, writeBS)
+import Snap.Http.Server ( httpServe, setAccessLog, setErrorLog, addListen
+ , ConfigListen (..), emptyConfig
+ )
+
+import Hakyll.Core.Util.String (replaceAll)
+
+-- | The first file in the list that actually exists is returned
+--
+findFile :: [FilePath] -> IO (Maybe FilePath)
+findFile [] = return Nothing
+findFile (x : xs) = do
+ exists <- doesFileExist x
+ if exists then return (Just x) else findFile xs
+
+-- | Serve a given directory
+--
+static :: FilePath -- ^ Directory to serve
+ -> (FilePath -> IO ()) -- ^ Pre-serve hook
+ -> Snap ()
+static directory preServe = do
+ -- Obtain the path
+ uri <- rqURI <$> getRequest
+ let filePath = replaceAll "\\?$" (const "") -- Remove trailing ?
+ $ replaceAll "#[^#]*$" (const "") -- Remove #section
+ $ replaceAll "^/" (const "") -- Remove leading /
+ $ decode $ SB.unpack uri
+
+ -- Try to find the requested file
+ r <- liftIO $ findFile $ map (directory </>) $
+ [ filePath
+ , filePath </> "index.htm"
+ , filePath </> "index.html"
+ ]
+
+ case r of
+ -- Not found, error
+ Nothing -> writeBS "Not found"
+ -- Found, serve
+ Just f -> do
+ liftIO $ preServe f
+ serveFile f
+
+-- | Main method, runs a static server in the given directory
+--
+staticServer :: FilePath -- ^ Directory to serve
+ -> (FilePath -> IO ()) -- ^ Pre-serve hook
+ -> Int -- ^ Port to listen on
+ -> IO () -- ^ Blocks forever
+staticServer directory preServe port =
+ httpServe config $ static directory preServe
+ where
+ -- Snap server config
+ config = addListen (ListenHttp "0.0.0.0" port)
+ $ setAccessLog Nothing
+ $ setErrorLog Nothing
+ $ emptyConfig
diff --git a/src/Hakyll/Web/RelativizeUrls.hs b/src/Hakyll/Web/RelativizeUrls.hs
new file mode 100644
index 0000000..2de4a0e
--- /dev/null
+++ b/src/Hakyll/Web/RelativizeUrls.hs
@@ -0,0 +1,62 @@
+-- | 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.RelativizeUrls
+ ( relativizeUrlsCompiler
+ , relativizeUrls
+ ) where
+
+import Prelude hiding (id)
+import Control.Category (id)
+import Control.Arrow ((&&&), (>>^))
+import Data.List (isPrefixOf)
+import qualified Data.Set as S
+
+import Text.HTML.TagSoup
+
+import Hakyll.Core.Compiler
+import Hakyll.Web.Page
+import Hakyll.Web.Util.Url
+
+-- | Compiler form of 'compressCss' which automatically picks the right root
+-- path
+--
+relativizeUrlsCompiler :: Compiler (Page String) (Page String)
+relativizeUrlsCompiler = getRoute &&& id >>^ uncurry relativize
+ where
+ relativize Nothing = id
+ relativize (Just r) = fmap (relativizeUrls $ toSiteRoot r)
+
+-- | Relativize URL's in HTML
+--
+relativizeUrls :: String -- ^ Path to the site root
+ -> String -- ^ HTML to relativize
+ -> String -- ^ Resulting HTML
+relativizeUrls root = renderTags . map relativizeUrls' . parseTags
+ where
+ relativizeUrls' (TagOpen s a) = TagOpen s $ map (relativizeUrlsAttrs root) a
+ relativizeUrls' x = x
+
+-- | Relativize URL's in attributes
+--
+relativizeUrlsAttrs :: String -- ^ Path to the site root
+ -> Attribute String -- ^ Attribute to relativize
+ -> Attribute String -- ^ Resulting attribute
+relativizeUrlsAttrs root (key, value)
+ | key `S.member` urls && "/" `isPrefixOf` value = (key, root ++ value)
+ | otherwise = (key, value)
+ where
+ urls = S.fromList ["src", "href"]
diff --git a/src/Hakyll/Web/Tags.hs b/src/Hakyll/Web/Tags.hs
new file mode 100644
index 0000000..211a06b
--- /dev/null
+++ b/src/Hakyll/Web/Tags.hs
@@ -0,0 +1,180 @@
+-- | Module containing some specialized functions to deal with tags.
+-- This Module follows certain conventions. My advice is to stick with them if
+-- possible.
+--
+-- More concrete: all functions in this module assume that the tags are
+-- located in the @tags@ field, and separated by commas. An example file
+-- @foo.markdown@ could look like:
+--
+-- > ---
+-- > author: Philip K. Dick
+-- > title: Do androids dream of electric sheep?
+-- > tags: future, science fiction, humanoid
+-- > ---
+-- > The novel is set in a post-apocalyptic near future, where the Earth and
+-- > its populations have been damaged greatly by Nuclear...
+--
+-- All the following functions would work with such a format. In addition to
+-- tags, Hakyll also supports categories. The convention when using categories
+-- is to place pages in subdirectories.
+--
+-- An example, the page @posts\/coding\/2010-01-28-hakyll-categories.markdown@
+-- Tags or categories are read using the @readTags@ and @readCategory@
+-- functions. This module only provides functions to work with tags:
+-- categories are represented as tags. This is perfectly possible: categories
+-- only have an additional restriction that a page can only have one category
+-- (instead of multiple tags).
+--
+{-# LANGUAGE DeriveDataTypeable, OverloadedStrings, Arrows #-}
+module Hakyll.Web.Tags
+ ( Tags (..)
+ , readTagsWith
+ , readTags
+ , readCategory
+ , renderTagCloud
+ , renderTagsField
+ , renderCategoryField
+ ) where
+
+import Prelude hiding (id)
+import Control.Category (id)
+import Control.Applicative ((<$>))
+import Data.Map (Map)
+import qualified Data.Map as M
+import Data.List (intersperse)
+import Control.Arrow (arr, (&&&), (>>>), (***), (<<^), returnA)
+import Data.Maybe (catMaybes, fromMaybe)
+import Data.Monoid (mconcat)
+
+import Data.Typeable (Typeable)
+import Data.Binary (Binary, get, put)
+import Text.Blaze.Renderer.String (renderHtml)
+import Text.Blaze ((!), toHtml, toValue)
+import qualified Text.Blaze.Html5 as H
+import qualified Text.Blaze.Html5.Attributes as A
+
+import Hakyll.Web.Page
+import Hakyll.Web.Page.Metadata
+import Hakyll.Web.Util.Url
+import Hakyll.Core.Writable
+import Hakyll.Core.Identifier
+import Hakyll.Core.Compiler
+import Hakyll.Core.Util.String
+
+-- | Data about tags
+--
+data Tags a = Tags
+ { tagsMap :: Map String [Page a]
+ } deriving (Show, Typeable)
+
+instance Binary a => Binary (Tags a) where
+ get = Tags <$> get
+ put (Tags m) = put m
+
+instance Writable (Tags a) where
+ write _ _ = return ()
+
+-- | Obtain tags from a page
+--
+getTags :: Page a -> [String]
+getTags = map trim . splitAll "," . getField "tags"
+
+-- | Obtain categories from a page
+--
+getCategory :: Page a -> [String]
+getCategory = return . getField "category"
+
+-- | Higher-level function to read tags
+--
+readTagsWith :: (Page a -> [String]) -- ^ Function extracting tags from a page
+ -> [Page a] -- ^ Pages
+ -> Tags a -- ^ Resulting tags
+readTagsWith f pages = Tags
+ { tagsMap = foldl (M.unionWith (++)) M.empty (map readTagsWith' pages)
+ }
+ where
+ -- Create a tag map for one page
+ readTagsWith' page =
+ let tags = f page
+ in M.fromList $ zip tags $ repeat [page]
+
+-- | Read a tagmap using the @tags@ metadata field
+--
+readTags :: [Page a] -> Tags a
+readTags = readTagsWith getTags
+
+-- | Read a tagmap using the @category@ metadata field
+--
+readCategory :: [Page a] -> Tags a
+readCategory = readTagsWith getCategory
+
+-- | Render a tag cloud in HTML
+--
+renderTagCloud :: (String -> Identifier) -- ^ Produce a link for a tag
+ -> Double -- ^ Smallest font size, in percent
+ -> Double -- ^ Biggest font size, in percent
+ -> Compiler (Tags a) String -- ^ Tag cloud renderer
+renderTagCloud makeUrl minSize maxSize = proc (Tags tags) -> do
+ -- In tags' we create a list: [((tag, route), count)]
+ tags' <- mapCompiler ((id &&& (getRouteFor <<^ makeUrl)) *** arr length)
+ -< M.toList tags
+
+ let -- Absolute frequencies of the pages
+ freqs = map snd tags'
+
+ -- Find out the relative count of a tag: on a scale from 0 to 1
+ relative count = (fromIntegral count - min') / (1 + max' - min')
+
+ -- Show the relative size of one 'count' in percent
+ size count =
+ let size' = floor $ minSize + relative count * (maxSize - minSize)
+ in show (size' :: Int) ++ "%"
+
+ -- The minimum and maximum count found, as doubles
+ (min', max')
+ | null freqs = (0, 1)
+ | otherwise = (minimum &&& maximum) $ map fromIntegral freqs
+
+ -- Create a link for one item
+ makeLink ((tag, url), count) =
+ H.a ! A.style (toValue $ "font-size: " ++ size count)
+ ! A.href (toValue $ fromMaybe "/" url)
+ $ toHtml tag
+
+ -- Render and return the HTML
+ returnA -< renderHtml $ mconcat $ intersperse " " $ map makeLink tags'
+
+-- | Render tags with links
+--
+renderTagsFieldWith :: (Page a -> [String]) -- ^ Function to get the tags
+ -> String -- ^ Destination key
+ -> (String -> Identifier) -- ^ Create a link for a tag
+ -> Compiler (Page a) (Page a) -- ^ Resulting compiler
+renderTagsFieldWith tags destination makeUrl =
+ id &&& arr tags >>> setFieldA destination renderTags
+ where
+ -- Compiler creating a comma-separated HTML string for a list of tags
+ renderTags :: Compiler [String] String
+ renderTags = arr (map $ id &&& makeUrl)
+ >>> mapCompiler (id *** getRouteFor)
+ >>> arr (map $ uncurry renderLink)
+ >>> arr (renderHtml . mconcat . intersperse ", " . catMaybes)
+
+ -- Render one tag link
+ renderLink _ Nothing = Nothing
+ renderLink tag (Just filePath) = Just $
+ H.a ! A.href (toValue $ toUrl filePath) $ toHtml tag
+
+-- | Render tags with links
+--
+renderTagsField :: String -- ^ Destination key
+ -> (String -> Identifier) -- ^ Create a link for a tag
+ -> Compiler (Page a) (Page a) -- ^ Resulting compiler
+renderTagsField = renderTagsFieldWith getTags
+
+-- | Render the category in a link
+--
+renderCategoryField :: String -- ^ Destination key
+ -> (String -> Identifier) -- ^ Create a category link
+ -> Compiler (Page a) (Page a) -- ^ Resulting compiler
+renderCategoryField = renderTagsFieldWith getCategory
diff --git a/src/Hakyll/Web/Template.hs b/src/Hakyll/Web/Template.hs
new file mode 100644
index 0000000..9c49278
--- /dev/null
+++ b/src/Hakyll/Web/Template.hs
@@ -0,0 +1,109 @@
+-- | This module provides means for reading and applying 'Template's.
+--
+-- Templates are tools to convert data (pages) 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>
+--
+-- We can use this template to render a 'Page' which has a body and a @$title$@
+-- metadata field.
+--
+-- 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.
+--
+-- In addition to the native format, Hakyll also supports hamlet templates. For
+-- more information on hamlet templates, please refer to:
+-- <http://hackage.haskell.org/package/hamlet>.
+--
+module Hakyll.Web.Template
+ ( Template
+ , applyTemplate
+ , applySelf
+ , templateCompiler
+ , templateCompilerWith
+ , applyTemplateCompiler
+ ) where
+
+import Control.Arrow
+import Data.Maybe (fromMaybe)
+import System.FilePath (takeExtension)
+import qualified Data.Map as M
+
+import Text.Hamlet (HamletSettings, defaultHamletSettings)
+
+import Hakyll.Core.Compiler
+import Hakyll.Core.Identifier
+import Hakyll.Core.ResourceProvider
+import Hakyll.Web.Template.Internal
+import Hakyll.Web.Template.Read
+import Hakyll.Web.Page.Internal
+
+-- | Substitutes @$identifiers@ in the given @Template@ by values from the given
+-- "Page". When a key is not found, it is left as it is. You can specify
+-- the characters used to replace escaped dollars (@$$@) here.
+--
+applyTemplate :: Template -> Page String -> Page String
+applyTemplate template page =
+ fmap (const $ substitute =<< unTemplate template) page
+ where
+ map' = toMap page
+ substitute (Chunk chunk) = chunk
+ substitute (Key key) = fromMaybe ("$" ++ key ++ "$") $ M.lookup key map'
+ substitute (Escaped) = "$"
+
+-- | Apply a page as it's own template. This is often very useful to fill in
+-- certain keys like @$root@ and @$url@.
+--
+applySelf :: Page String -> Page String
+applySelf page = applyTemplate (readTemplate $ pageBody page) page
+
+-- | Read a template. If the extension of the file we're compiling is
+-- @.hml@ or @.hamlet@, it will be considered as a Hamlet template, and parsed
+-- as such.
+--
+templateCompiler :: Compiler Resource Template
+templateCompiler = templateCompilerWith defaultHamletSettings
+
+-- | Version of 'templateCompiler' that enables custom settings.
+--
+templateCompilerWith :: HamletSettings -> Compiler Resource Template
+templateCompilerWith settings =
+ cached "Hakyll.Web.Template.templateCompilerWith" $
+ getIdentifier &&& getResourceString >>^ uncurry read'
+ where
+ read' identifier string =
+ if takeExtension (toFilePath identifier) `elem` [".hml", ".hamlet"]
+ -- Hamlet template
+ then readHamletTemplateWith settings string
+ -- Hakyll template
+ else readTemplate string
+
+applyTemplateCompiler :: Identifier -- ^ Template
+ -> Compiler (Page String) (Page String) -- ^ Compiler
+applyTemplateCompiler identifier = require identifier (flip applyTemplate)
diff --git a/src/Hakyll/Web/Template/Internal.hs b/src/Hakyll/Web/Template/Internal.hs
new file mode 100644
index 0000000..d0e0859
--- /dev/null
+++ b/src/Hakyll/Web/Template/Internal.hs
@@ -0,0 +1,45 @@
+-- | Module containing the template data structure
+--
+{-# LANGUAGE GeneralizedNewtypeDeriving, DeriveDataTypeable #-}
+module Hakyll.Web.Template.Internal
+ ( Template (..)
+ , TemplateElement (..)
+ ) where
+
+import Control.Applicative ((<$>))
+
+import Data.Binary (Binary, get, getWord8, put, putWord8)
+import Data.Typeable (Typeable)
+
+import Hakyll.Core.Writable
+
+-- | 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 ()
+
+-- | Elements of a template.
+--
+data TemplateElement
+ = Chunk String
+ | Key String
+ | Escaped
+ deriving (Show, Eq, Typeable)
+
+instance Binary TemplateElement where
+ put (Chunk string) = putWord8 0 >> put string
+ put (Key key) = putWord8 1 >> put key
+ put (Escaped) = putWord8 2
+
+ get = getWord8 >>= \tag -> case tag of
+ 0 -> Chunk <$> get
+ 1 -> Key <$> get
+ 2 -> return Escaped
+ _ -> error $ "Hakyll.Web.Template.Internal: "
+ ++ "Error reading cached template"
diff --git a/src/Hakyll/Web/Template/Read.hs b/src/Hakyll/Web/Template/Read.hs
new file mode 100644
index 0000000..421b7e9
--- /dev/null
+++ b/src/Hakyll/Web/Template/Read.hs
@@ -0,0 +1,10 @@
+-- | Re-exports all different template reading modules
+--
+module Hakyll.Web.Template.Read
+ ( readTemplate
+ , readHamletTemplate
+ , readHamletTemplateWith
+ ) where
+
+import Hakyll.Web.Template.Read.Hakyll
+import Hakyll.Web.Template.Read.Hamlet
diff --git a/src/Hakyll/Web/Template/Read/Hakyll.hs b/src/Hakyll/Web/Template/Read/Hakyll.hs
new file mode 100644
index 0000000..fecf772
--- /dev/null
+++ b/src/Hakyll/Web/Template/Read/Hakyll.hs
@@ -0,0 +1,35 @@
+-- | Read templates in Hakyll's native format
+--
+module Hakyll.Web.Template.Read.Hakyll
+ ( readTemplate
+ ) where
+
+import Data.List (isPrefixOf)
+import Data.Char (isAlphaNum)
+
+import Hakyll.Web.Template.Internal
+
+-- | Construct a @Template@ from a string.
+--
+readTemplate :: String -> Template
+readTemplate = Template . readTemplate'
+ where
+ readTemplate' [] = []
+ readTemplate' string
+ | "$$" `isPrefixOf` string =
+ Escaped : readTemplate' (drop 2 string)
+ | "$" `isPrefixOf` string =
+ case readKey (drop 1 string) of
+ Just (key, rest) -> Key key : readTemplate' rest
+ Nothing -> Chunk "$" : readTemplate' (drop 1 string)
+ | otherwise =
+ let (chunk, rest) = break (== '$') string
+ in Chunk chunk : readTemplate' rest
+
+ -- Parse an key into (key, rest) if it's valid, and return
+ -- Nothing otherwise
+ readKey string =
+ let (key, rest) = span isAlphaNum string
+ in if not (null key) && "$" `isPrefixOf` rest
+ then Just (key, drop 1 rest)
+ else Nothing
diff --git a/src/Hakyll/Web/Template/Read/Hamlet.hs b/src/Hakyll/Web/Template/Read/Hamlet.hs
new file mode 100644
index 0000000..7b496de
--- /dev/null
+++ b/src/Hakyll/Web/Template/Read/Hamlet.hs
@@ -0,0 +1,46 @@
+-- | Read templates in the hamlet format
+--
+{-# LANGUAGE MultiParamTypeClasses #-}
+module Hakyll.Web.Template.Read.Hamlet
+ ( readHamletTemplate
+ , readHamletTemplateWith
+ ) where
+
+import Text.Hamlet (HamletSettings (..), defaultHamletSettings)
+import Text.Hamlet.RT
+
+import Hakyll.Web.Template.Internal
+
+-- | Read a hamlet template using the default settings
+--
+readHamletTemplate :: String -> Template
+readHamletTemplate = readHamletTemplateWith defaultHamletSettings
+
+-- | Read a hamlet template using the specified settings
+--
+readHamletTemplateWith :: HamletSettings -> String -> Template
+readHamletTemplateWith settings string =
+ let result = parseHamletRT settings string
+ in case result of
+ Just hamlet -> fromHamletRT hamlet
+ Nothing -> error
+ "Hakyll.Web.Template.Read.Hamlet.readHamletTemplateWith: \
+ \Could not parse Hamlet file"
+
+-- | Convert a 'HamletRT' to a 'Template'
+--
+fromHamletRT :: HamletRT -- ^ Hamlet runtime template
+ -> Template -- ^ Hakyll template
+fromHamletRT (HamletRT sd) = Template $ map fromSimpleDoc sd
+ where
+ fromSimpleDoc :: SimpleDoc -> TemplateElement
+ fromSimpleDoc (SDRaw chunk) = Chunk chunk
+ fromSimpleDoc (SDVar [var]) = Key var
+ fromSimpleDoc (SDVar _) = error
+ "Hakyll.Web.Template.Read.Hamlet.fromHamletRT: \
+ \Hakyll does not support '.' in identifier names when using \
+ \hamlet templates."
+ fromSimpleDoc _ = error
+ "Hakyll.Web.Template.Read.Hamlet.fromHamletRT: \
+ \Only simple $key$ identifiers are allowed when using hamlet \
+ \templates."
diff --git a/src/Hakyll/Web/Util/Url.hs b/src/Hakyll/Web/Util/Url.hs
new file mode 100644
index 0000000..54a361e
--- /dev/null
+++ b/src/Hakyll/Web/Util/Url.hs
@@ -0,0 +1,30 @@
+-- | Miscellaneous URL manipulation functions.
+--
+module Hakyll.Web.Util.Url
+ ( toUrl
+ , toSiteRoot
+ ) where
+
+import System.FilePath (splitPath, takeDirectory, joinPath)
+
+-- | Convert a filepath to an URL starting from the site root
+--
+-- Example:
+--
+-- > toUrl "foo/bar.html"
+--
+-- Result:
+--
+-- > "/foo/bar.html"
+--
+toUrl :: FilePath -> String
+toUrl = ('/' :)
+
+-- | Get the relative url to the site root, for a given (absolute) url
+--
+toSiteRoot :: String -> String
+toSiteRoot = emptyException . joinPath . map parent . splitPath . takeDirectory
+ where
+ parent = const ".."
+ emptyException [] = "."
+ emptyException x = x