packages feed

hakyll-contrib-links-0.2.0.0: Hakyll/Contrib/Markdown/Link.hs

{-|

Set of Hakyll rules that facilitate including links to markdown files.

-}

{-# LANGUAGE DeriveDataTypeable         #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE Arrows                     #-}

module Hakyll.Contrib.Markdown.Link
       ( -- * Syntax
         -- $syntax
         Link(..)
       , links
       , toString
       , linksCompiler
       , pageWithLinksCompiler
       , include
       , includeList
       , pageCompilerA, readStatePage
       ) where

import Control.Arrow
import Control.Applicative hiding ((<|>))
import Data.Binary
import Data.Typeable
import Data.Char           (isSpace, isAlphaNum)
import Data.List           (groupBy, isPrefixOf)
import Data.Map            (fromList, union)
import Data.String
import Hakyll.Core.Compiler(getResourceString, Compiler, requireA, cached)
import Hakyll.Core.Identifier(Identifier)
import Hakyll.Core.Resource(Resource)
import Hakyll.Core.Writable(Writable(..))
import Hakyll.Web.Pandoc ( defaultHakyllParserState
                         , defaultHakyllWriterOptions
                         , pageReadPandocWithA
                         , writePandocWith
                         )
import Hakyll.Web.Template(applySelf)
import Hakyll.Web.Page(addDefaultFields, readPageCompiler, Page)
import Text.Pandoc(WriterOptions)
import Text.Pandoc.Parsing(toKey, ParserState(..))
import Text.Pandoc.Definition hiding (Link)
import Text.Parsec.String  (Parser)
import Text.Parsec



-- | The link data type.
data Link = Link { linkRef     :: [String] -- ^ reference to this link
                 , linkUrl     ::  String  -- ^ destination url
                 , linkTitle   :: [String] -- ^ link title string.
                 } deriving (Show, Eq, Typeable)

instance IsString Link where
  fromString s = either err id $ parse linkP "<>" s
        where err e = error $ "bad link:" ++ show e


instance Binary Link where
  put lnk = do put $ linkRef lnk
               put $ linkUrl lnk
               put $ linkTitle lnk
  get     = Link <$> get <*> get <*> get

instance Writable [Link] where
  write fp = write fp . unlines . map toString

-- | Cache name used by this module.
cacheName :: String
cacheName = "Hakyll.Contrib.Markdown.Link"

-- | The Hakyll compiler that reads a list of links.
linksCompiler :: Compiler Resource [Link]
linksCompiler = cached cacheName $ getResourceString >>> arr links

-- | Compiles a pandoc page with a give list of links database.
pageWithLinksCompiler :: [Identifier [Link]] -- ^ The links db's
                      -> Compiler Resource (Page String)
pageWithLinksCompiler = pageWithLinksCompiler' defaultHakyllParserState
                                               defaultHakyllWriterOptions

-- | Similar to `pageWithLinksCompiler` but takes additional parser
-- and writer option.
pageWithLinksCompiler' :: ParserState         -- ^ parser state
                       -> WriterOptions       -- ^ pandoc writer options
                       -> [Identifier [Link]] -- ^ The links db's
                       -> Compiler Resource (Page String)
pageWithLinksCompiler' popts wopts ids = cached cacheName $
      readStatePage popts >>> includeList ids >>> pageCompilerA wopts

-- | A compiler that modified the Pandoc parser state to accomodate a
-- set of link definitions.
include :: Identifier [Link]  -- ^ The link database identifier
        -> Compiler (ParserState,a) (ParserState,a)
include ident = first $ requireA ident $ arr combine
   where combine (ps,lns)= ps { stateKeys = stateKeys ps `union`
                                            toKeyTable lns
                              }
         toKeyTable = fromList . map kvPair
         kvPair (Link r u t) = (toKey $ map Str r, (u, unwords t))


-- | Similar to include but includes a list of link definitions.
includeList :: [Identifier [Link]] -- ^ The list of link database
                                   -- identifiers.
            -> Compiler (ParserState,a) (ParserState, a)

includeList =  foldl1 (>>>) . map include

-- | Tags along a given parser state to the page.
readStatePage :: ParserState
              -> Compiler Resource (ParserState, Page String)
readStatePage pstate = proc res -> do page    <- readPageCompiler -< res
                                      returnA -< (pstate, page)

-- | Compiles a given page with pandoc. The writer option can be
-- configured using its argument.
pageCompilerA :: WriterOptions -- ^ Configure writer options
              -> Compiler (ParserState, Page String) (Page String)
pageCompilerA wopts = second fieldsAndTemplate >>> pageReadPandocWithA >>> writer
   where fieldsAndTemplate = addDefaultFields >>> arr applySelf
         writer    = arr $ fmap $ writePandocWith wopts

-- | Convert the link to string.
toString :: Link -> String
toString lnk = concat [ "[", r, "]"
                      , ": "
                      , "<", linkUrl lnk, ">"
                      , t
                      ]
   where r = unwords $ map escapeRef   $ linkRef   lnk
         t = unwords $ map escapeTitle $ linkTitle lnk


escapeRef :: String -> String
escapeRef = concatMap esc
  where esc c | isAlphaNum c || c `elem` refSpChars = [c]
              | otherwise                           = escapeChar : [c]

escapeTitle :: String -> String
escapeTitle = concatMap esc
  where esc c | isSpace c = escapeChar : [c]
              | otherwise = [c]

-- | Link parser
linkP  :: Parser Link


-- | Convert a parser to a lexeme parser.
lexeme :: Parser a -> Parser a
lexeme p = p <* spaces

-- | An escape char
escape :: Parser Char
escape = char escapeChar >> anyChar

-- | The special characters in references.
refSpChars :: [Char]
refSpChars = "#_-:"

-- | A link reference
ref :: Parser [String]
ref     = many1 refWord <?> "link-reference"
   where refChar = escape <|> alphaNum <|> oneOf refSpChars
         refWord = lexeme $ many1 refChar

-- | A url
url  :: Parser String
url  = lexeme (between langle rangle $ many1 $ noneOf ">")
       <?> "URL"
  where langle  = lexeme $ char '<'
        rangle  = lexeme $ char '>'

-- | A link title
title :: Parser [String]
title = many1 titleWord <?> "link-title"
  where titleChar = escape <|> satisfy (not . isSpace)
        titleWord = lexeme $ many1 titleChar

linkP = Link <$> lref <* colon <*> url <*> option [] title
  where lref    = between lbrack rbrack ref <?> "[link-reference]"
        colon    = lexeme $ char ':'
        lbrack   = lexeme $ char '['
        rbrack   = lexeme $ char ']'


-- $syntax
--
-- We use the markdown syntax to provide links namely
--
-- > [linkname]: <url> Comment
--
-- Multiple lines are folded if the are indented by atleast by a
-- space.  All lines that start with a @'--'@ (two hyphens) are
-- treated as comments and skipped.

-- | Convert a set of lines to links.
links :: String -> [Link]
links = map fromString
      . map combine
      . indent
      . filter (not . commentLine)
      . filter (not . null)
      . lines
  where indent = groupBy (\ _ -> isSpace . head)
        combine []     = []
        combine (x:xs) = concat (x: map tail xs)
        commentLine    = isPrefixOf commentStart

escapeChar   :: Char
commentStart :: String
escapeChar   = '\\'
commentStart = "--"