diff options
Diffstat (limited to 'src/Hakyll/Web')
-rw-r--r-- | src/Hakyll/Web/CompressCss.hs | 51 | ||||
-rw-r--r-- | src/Hakyll/Web/Feed.hs | 124 | ||||
-rw-r--r-- | src/Hakyll/Web/FileType.hs | 55 | ||||
-rw-r--r-- | src/Hakyll/Web/Page.hs | 124 | ||||
-rw-r--r-- | src/Hakyll/Web/Page/Internal.hs | 50 | ||||
-rw-r--r-- | src/Hakyll/Web/Page/Metadata.hs | 131 | ||||
-rw-r--r-- | src/Hakyll/Web/Page/Read.hs | 60 | ||||
-rw-r--r-- | src/Hakyll/Web/Pandoc.hs | 110 | ||||
-rw-r--r-- | src/Hakyll/Web/Preview/Server.hs | 72 | ||||
-rw-r--r-- | src/Hakyll/Web/RelativizeUrls.hs | 62 | ||||
-rw-r--r-- | src/Hakyll/Web/Tags.hs | 180 | ||||
-rw-r--r-- | src/Hakyll/Web/Template.hs | 109 | ||||
-rw-r--r-- | src/Hakyll/Web/Template/Internal.hs | 45 | ||||
-rw-r--r-- | src/Hakyll/Web/Template/Read.hs | 10 | ||||
-rw-r--r-- | src/Hakyll/Web/Template/Read/Hakyll.hs | 35 | ||||
-rw-r--r-- | src/Hakyll/Web/Template/Read/Hamlet.hs | 46 | ||||
-rw-r--r-- | src/Hakyll/Web/Util/Url.hs | 30 |
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 |