hindent-6.0.0: internal/HIndent/Internal/Test/Markdone.hs
{-# LANGUAGE DeriveDataTypeable #-}
{-# 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 HIndent.Internal.Test.Markdone
( Token(..)
, Markdone(..)
, tokenize
, parse
) where
import Control.DeepSeq
import Control.Monad.Catch
import Control.Monad.State.Strict (State, evalState, get, put)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as S8
import Data.Char
import Data.Typeable
import GHC.Generics
-- | A markdone token.
data Token
= Heading !Int !ByteString
| PlainLine !ByteString
| BeginFence !ByteString
| EndFence
deriving (Eq, Show)
-- | A markdone document.
data Markdone
= Section !ByteString ![Markdone]
| CodeFence !ByteString !ByteString
| PlainText !ByteString
deriving (Eq, Show, Generic)
instance NFData Markdone
-- | Parse error.
data MarkdownError
= NoFenceEnd
| ExpectedSection
deriving (Typeable, Show)
instance Exception MarkdownError
data TokenizerMode
= Normal
| Fenced
-- | Tokenize the bytestring.
tokenize :: ByteString -> [Token]
tokenize input =
evalState (mapM token (S8.filter (/= '\r') <$> S8.lines input)) Normal
where
token :: ByteString -> State TokenizerMode Token
token line = do
mode <- get
case mode of
Normal ->
if S8.isPrefixOf "#" line
then let (hashes, title) = S8.span (== '#') line
in return $
Heading (S8.length hashes) (S8.dropWhile isSpace title)
else if S8.isPrefixOf "```" line
then do
put Fenced
return $
BeginFence
(S8.dropWhile (\c -> c == '`' || c == ' ') line)
else return $ PlainLine line
Fenced ->
if line == "```"
then do
put Normal
return EndFence
else return $ PlainLine line
-- | Parse into a forest.
parse :: (Functor m, 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
(S8.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
(S8.intercalate
"\n"
(filter (not . S8.null) (map getPlain content))) :)
(go level rest')
[] -> return []
_ -> throwM ExpectedSection
getPlain (PlainLine x) = x
getPlain _ = ""