floskell-0.11.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.Char
import Data.Monoid ( (<>) )
import Data.Text.Lazy ( Text )
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as TB
import qualified Data.Text.Lazy.IO as TIO
import Data.Typeable
import GHC.Generics
-- | A markdone token.
data Token = Heading !Int !Text | PlainLine !Text | BeginFence !Text | EndFence
deriving ( Show )
-- | A markdone document.
data Markdone =
Section !Text ![Markdone] | CodeFence !Text !Text | PlainText !Text
deriving ( Show, Generic )
instance NFData Markdone
-- | Parse error.
data MarkdownError = NoFenceEnd | ExpectedSection
deriving ( Typeable, Show )
instance Exception MarkdownError
-- | Tokenize the bytestring.
tokenize :: Text -> [Token]
tokenize = map token . TL.lines
where
token line
| TL.isPrefixOf "#" line = let (hashes, title) = TL.span (== '#') line
in
Heading (fromIntegral $ TL.length hashes)
(TL.dropWhile isSpace title)
| TL.isPrefixOf "```" line =
if line == "```"
then EndFence
else BeginFence (TL.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
(TL.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 (TL.intercalate "\n" (map getPlain content)) :)
(go level rest')
[] -> return []
_ -> throwM ExpectedSection
getPlain (PlainLine x) = x
getPlain _ = ""
print :: [Markdone] -> TB.Builder
print = mconcat . map (go 0)
where
go level = \case
(Section heading children) ->
let level' = level + 1
in
TB.fromLazyText (TL.replicate level' "#") <> TB.singleton ' '
<> TB.fromLazyText heading <> TB.fromString "\n"
<> mconcat (map (go level') children)
(CodeFence lang code) ->
TB.fromString "``` " <> TB.fromLazyText lang <> TB.singleton '\n'
<> TB.fromLazyText code <> TB.fromString "\n```\n"
(PlainText text) -> TB.fromLazyText text <> TB.fromString "\n"