packages feed

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"