floskell-0.10.0: src/main/Markdone.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
-- | A subset of markdown that only supports @#headings@ and code
-- fences.
--
-- All content must be in section headings with proper hierarchy,
-- anything else is rejected.
module Markdone where
import Control.DeepSeq
import Control.Monad.Catch
import Data.ByteString.Builder as B
import qualified Data.ByteString.Char8 as S8
import Data.ByteString.Lazy ( ByteString )
import qualified Data.ByteString.Lazy.Char8 as L8
import Data.Char
import Data.Monoid ( (<>) )
import Data.Typeable
import GHC.Generics
-- | A markdone token.
data Token = Heading !Int !ByteString
| PlainLine !ByteString
| BeginFence !ByteString
| EndFence
deriving ( Show )
-- | A markdone document.
data Markdone = Section !ByteString ![Markdone]
| CodeFence !ByteString !ByteString
| PlainText !ByteString
deriving ( Show, Generic )
instance NFData Markdone
-- | Parse error.
data MarkdownError = NoFenceEnd | ExpectedSection
deriving ( Typeable, Show )
instance Exception MarkdownError
-- | Tokenize the bytestring.
tokenize :: ByteString -> [Token]
tokenize = map token . L8.lines
where
token line
| L8.isPrefixOf "#" line = let (hashes, title) = L8.span (== '#') line
in
Heading (fromIntegral $ L8.length hashes)
(L8.dropWhile isSpace title)
| L8.isPrefixOf "```" line =
if line == "```"
then EndFence
else BeginFence (L8.dropWhile (\c -> c == '`' || c == ' ') line)
| otherwise = PlainLine line
-- | Parse into a forest.
parse :: MonadThrow m => [Token] -> m [Markdone]
parse = go (0 :: Int)
where
go level = \case
(Heading n label : rest) ->
let (children, rest') = span (\case
Heading nextN _ -> nextN > n
_ -> True)
rest
in
do
childs <- go (level + 1) children
siblings <- go level rest'
return (Section label childs : siblings)
(BeginFence label : rest)
| level > 0 ->
let (content, rest') = span (\case
PlainLine{} -> True
_ -> False)
rest
in
case rest' of
(EndFence : rest'') ->
fmap (CodeFence label
(L8.intercalate "\n"
(map getPlain
content)) :)
(go level rest'')
_ -> throwM NoFenceEnd
PlainLine p : rest
| level > 0 ->
let (content, rest') = span (\case
PlainLine{} -> True
_ -> False)
(PlainLine p : rest)
in
fmap (PlainText (L8.intercalate "\n" (map getPlain content)) :)
(go level rest')
[] -> return []
_ -> throwM ExpectedSection
getPlain (PlainLine x) = x
getPlain _ = ""
print :: [Markdone] -> B.Builder
print = mconcat . map (go (0 :: Int))
where
go level = \case
(Section heading children) ->
let level' = level + 1
in
B.byteString (S8.replicate level' '#') <> B.char7 ' '
<> B.lazyByteString heading <> B.byteString "\n"
<> mconcat (map (go level') children)
(CodeFence lang code) -> B.byteString "``` " <> B.lazyByteString lang
<> B.char7 '\n' <> B.lazyByteString code <> B.byteString "\n```\n"
(PlainText text) -> B.lazyByteString text <> B.byteString "\n"