gdritter repos telml-markup / master Data / TeLML / Markup.hs
master

Tree @master (Download .tar.gz)

Markup.hs @masterraw · history · blame

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

module Data.TeLML.Markup where

import Control.Monad (void)
import Data.TeLML
import Text.Blaze.Html
import Text.Blaze.Html5 hiding (map, head, html)
import Text.Blaze.Html5.Attributes hiding (name, span)

import Prelude hiding (div, span)

-- | Render a TeLML document with an extra set of possible tags.
renderWith :: [(String, Renderer)] -> Document -> Either String Html
renderWith rs =
  fmap (void . sequence) . mapM (renderPara (basicTags ++ rs)) . gatherPara

-- | Render a TeLML document with the default set of tags.
render :: Document -> Either String Html
render = renderWith []

-- This is a gross function, but I'm not sure how to decompose it any
-- other way. It takes a Document---i.e. a set of Fragments---and
-- splits it apart whenever it comes across double newlines.
gatherPara :: Document -> [Document]
gatherPara = reverse . map reverse . go [[]]
  where go rs [] = rs
        go (r:rs) (t@Tag {}:ts) = go ((t:r):rs) ts
        go (r:rs) (Text s:ts)   = case splitString s of
          []  -> go (r:rs) ts
          [x] -> go ((Text x:r):rs) ts
          xs  -> go (map ((:[]) . Text) (tail xs) ++
                     ((Text (head xs):r) : rs)) ts
        go _ _ = error "[unreachable]"

-- Split a string at double-newlines.
splitString :: String -> [String]
splitString = filter (/= "") . go
  where go ('\n':'\n':xs) = "\n":go xs
        go (x:xs)         = let r:rs = go xs in ((x:r):rs)
        go ""             = [""]

-- This is just to make type signatures shorter
type HtmlE = Either String Html

type Renderer = (Fragment -> HtmlE, [Document]) -> HtmlE

-- The built-in set of tags (subject to change)
basicTags :: [(String, Renderer)]
basicTags =
  [ simpleTag "em" em
  , simpleTag "strong" strong
  , simpleTag "li" li
  , simpleTag "h1" h1
  , simpleTag "h2" h2
  , simpleTag "p" (\ rs -> span ! class_ "para" $ rs)
  , simpleTag "blockquote" blockquote
  , simpleTag "tt" code
  , simpleTag "code" (pre . code)
  , simpleTag "ttcom" (\ rs -> span ! class_ "comment" $ rs)
  , simpleTag "ttkw"  (\ rs -> span ! class_ "keyword" $ rs)
  , simpleTag "ttcn"  (\ rs -> span ! class_ "constr" $ rs)
  , simpleTag "ttstr" (\ rs -> span ! class_ "string" $ rs)
  , listTag "ul" ul
  , listTag "ol" ol
  , listTag "center" (\ rs -> div ! class_ "center" $ rs)
  , ("br", \_ -> return br)
  , ("comment", \_ -> return "")
  , ("link"
    , \case (f,[[Text l],r]) -> let go h = a ! href (stringValue l) $ h
                                in fmap (go . sequence_) (mapM f r)
            (_,[_,_])        -> Left "link target should be string"
            _                -> Left "wrong arity for link/1"
    )
  , ("img"
    , \case (_, [[Text l]]) -> return (img ! src (stringValue l))
            (_,[_])         -> Left "image target should be string"
            _               -> Left "wrong arity for img/1"
    )
  ]
  where simpleTag :: String -> (Html -> Html) -> (String, Renderer)
        simpleTag name tag =
          ( name
          , \case (f,[rs]) -> fmap (tag . sequence_) (mapM f rs)
                  _        -> Left ("wrong arity for " ++ name ++ "/1")
          )
        listTag name tag =
          ( name
          , \case (f,rs) -> fmap (tag . sequence_) (mapM f (concat rs))
          )

-- render a single paragraph
renderPara :: [(String, Renderer)] -> Document -> Either String Html
renderPara taglist ds = fmap (p . sequence_) (mapM go ds)
  where go (Text ts) = Right (toMarkup ts)
        go (Tag tx rs) = exec tx rs taglist
        exec name args ((tag, func):tags)
          | name == tag = case func (go, args) of
            Right html -> Right html
            Left {}    -> exec name args tags
        exec name args (_:tags) = exec name args tags
        exec name args [] = Left $
          "Error: no match for tag " ++ name ++ "/" ++ show (length args)