hablo/src/Markdown.hs

77 lines
2.3 KiB
Haskell

{-# LANGUAGE NamedFieldPuns #-}
module Markdown (
Markdown(..)
, MarkdownContent(..)
, Metadata
, at
, getKey
) where
import Control.Applicative ((<|>))
import Data.Map (Map)
import qualified Data.Map as Map (fromList)
import System.FilePath (dropExtension, takeFileName)
import Text.ParserCombinators.Parsec (
ParseError, Parser
, (<?>)
, anyChar, char, count, endBy, eof, getPosition, many, many1, noneOf
, oneOf, option, parse, skipMany, sourceLine, sourceName, string, try
)
type Metadata = Map String String
data Markdown = Markdown {
key :: String
, path :: String
, title :: String
, metadata :: Metadata
, bodyOffset :: Int
, body :: [String]
}
class MarkdownContent a where
getMarkdown :: a -> Markdown
parser :: Parser Markdown
parser = do
(title, metadata) <- skipMany eol *> (headerP <|> reverseHeaderP)
bodyOffset <- skipMany eol *> (pred . sourceLine <$> getPosition)
body <- lines <$> many anyChar <* eof
inputFile <- sourceName <$> getPosition
let (key, path) = (getKey inputFile, dropExtension inputFile)
return $ Markdown {key, path, title, metadata, bodyOffset, body}
where
headerP = (,) <$> titleP <* many eol <*> metadataP
reverseHeaderP = flip (,) <$> metadataP <* many eol<*> titleP
metadataP :: Parser Metadata
metadataP = Map.fromList <$> option [] (
metaSectionSeparator *> many eol *>
(try keyVal) `endBy` (many1 eol)
<* metaSectionSeparator
) <?> "metadata section"
where
metaSectionSeparator = count 3 (oneOf "~-") *> eol
spaces = skipMany $ char ' '
keyVal = (,) <$> (no ": \r\n" <* spaces <* char ':' <* spaces) <*> no "\r\n"
titleP :: Parser String
titleP = try (singleLine <|> underlined)
where
singleLine = char '#' *> char ' ' *> no "\r\n" <* eol
underlined =
no "\r\n" <* eol
>>= \titleLine -> count (length titleLine) (oneOf "#=") *> eol *> return titleLine
<?> "'#' or '=' to underline the title"
eol :: Parser String
eol = try (string "\r\n") <|> string "\r" <|> string "\n" <?> "newline"
no :: String -> Parser String
no = many1 . noneOf
getKey :: FilePath -> String
getKey = dropExtension . takeFileName
at :: FilePath -> IO (Either ParseError Markdown)
at filePath = parse parser filePath <$> readFile filePath