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

Tree @master (Download .tar.gz)

Markup.hs @masterraw · history · blame

{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}

module Data.TeLML.Markup
  ( renderWith,
    render,
    basicTags,
    mkTag,
    simpleTag,
    listTag,
    H (..),
    Hs (..),
    Str (..),
    TagDescription,
  )
where

import Control.Monad (void)
import Data.TeLML
import qualified Data.Text as T
import Text.Blaze.Html
import Text.Blaze.Html5 hiding (head, html, map)
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 :: [TagDescription] -> 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@TagFrag {} : ts) = go ((t : r) : rs) ts
    go (r : rs) (TextFrag s : ts) = case splitString s of
      [] -> go (r : rs) ts
      [x] -> go ((TextFrag x : r) : rs) ts
      xs ->
        go
          ( map ((: []) . TextFrag) (tail xs)
              ++ ((TextFrag (head xs) : r) : rs)
          )
          ts
    go _ _ = error "[unreachable]"

-- Split a string at double-newlines.
splitString :: T.Text -> [T.Text]
splitString = T.splitOn "\n\n"

-- | The 'TagArguments' class allow us to define a new tag with a name
-- and a simple function, and cuts out a lot of the boilerplate.
class TagArguments t where
  toType :: t -> [T.Text]
  taExec ::
    t ->
    [Document] ->
    (Fragment -> Either String Html) ->
    Maybe (Either String Html)

instance TagArguments Html where
  toType _ = []
  taExec h [] _ = Just (Right h)
  taExec _ _ _ = Nothing

instance TagArguments r => TagArguments (Str -> r) where
  toType _ = "str" : toType (undefined :: r)
  taExec f ([TextFrag t] : rs) go = taExec (f (Str t)) rs go
  taExec _ _ _ = Nothing

instance TagArguments r => TagArguments (Maybe Str -> r) where
  toType _ = "str?" : toType (undefined :: r)
  taExec f ([TextFrag t] : rs) go = taExec (f (Just (Str t))) rs go
  taExec f [] go = taExec (f Nothing) [] go
  taExec _ _ _ = Nothing

instance TagArguments r => TagArguments (H -> r) where
  toType _ = "frag" : toType (undefined :: r)
  taExec f (doc : rs) go =
    let h = fmap sequence_ (mapM go doc)
     in case h of
          Left err -> return (Left err)
          Right h' -> taExec (f (H h')) rs go
  taExec _ [] _ = Nothing

instance (h ~ Html) => TagArguments (Hs -> h) where
  toType _ = ["..."]
  taExec f docs go =
    let h = mapM (fmap sequence_ . mapM go) docs
     in case h of
          Left err -> return (Left err)
          Right hs -> return (Right (f (Hs hs)))

data TagDescription
  = forall t.
    TagArguments t =>
    TagDescription T.Text t

-- | The 'Str' newtype will match a literal chunk of non-formatted,
-- non-structured text.
newtype Str = Str {fromStr :: T.Text}

-- | The 'H' newtype will match a single, pre-rendered argument
newtype H = H {fromHtml :: Html}

-- | The 'Hs' newtype will match a concatenated set of pre-rendered
-- arguments
newtype Hs = Hs {fromHtmlList :: [Html]}

mkTag :: TagArguments t => T.Text -> t -> TagDescription
mkTag = TagDescription

-- The built-in set of tags (subject to change)
basicTags :: [TagDescription]
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,
    mkTag "list" (\(Hs hs) -> ul $ mapM_ li hs),
    listTag "center" (\rs -> div ! class_ "center" $ rs),
    TagDescription "br" br,
    TagDescription "comment" ("" :: Html),
    TagDescription "link" (\(Str l) (H h) -> a ! href (toValue l) $ h),
    TagDescription "img" $ \(Str l) altText -> case altText of
      Just r -> img ! src (toValue l) ! alt (toValue (fromStr r))
      Nothing -> img ! src (toValue l)
  ]

simpleTag :: T.Text -> (Markup -> Html) -> TagDescription
simpleTag name tag = mkTag name (tag . fromHtml)

listTag :: T.Text -> (Markup -> Html) -> TagDescription
listTag name tag = mkTag name (tag . mconcat . fromHtmlList)

argsFor :: TagArguments t => t -> T.Text
argsFor func = T.cons '{' (T.snoc (T.intercalate "|" (toType func)) '}')

-- render a single paragraph
renderPara :: [TagDescription] -> Document -> Either String Html
renderPara taglist ds = fmap (p . sequence_) (mapM go ds)
  where
    go (TextFrag ts) = Right (toMarkup ts)
    go (TagFrag (Tag tx rs)) = exec tx rs taglist
    exec name args (TagDescription tag func : _)
      | name == tag = case taExec func args go of
        Nothing ->
          Left $
            unwords
              [ "Tag",
                T.unpack ('\\' `T.cons` name),
                "expects argument structure",
                T.unpack ('\\' `T.cons` name `T.append` argsFor func)
              ]
        Just x -> x
    exec name args (_ : rs) = exec name args rs
    exec name args [] =
      Left $
        "Error: no match for tag " ++ T.unpack name ++ "/" ++ show (length args)