matterhorn-50200.16.0: src/Matterhorn/Draw/RichText.hs
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE ParallelListComp #-}
module Matterhorn.Draw.RichText
( renderRichText
, renderText
, renderText'
, cursorSentinel
, findVerbatimChunk
)
where
import Prelude ()
import Matterhorn.Prelude
import Brick ( (<+>), Widget, hLimit, imageL
, render, Size(..), Widget(..)
)
import qualified Brick as B
import qualified Brick.Widgets.Border as B
import qualified Brick.Widgets.Table as B
import qualified Brick.Widgets.Skylighting as BS
import Control.Monad.Reader
import qualified Data.Foldable as F
import qualified Data.Sequence as S
import Data.Sequence ( ViewL(..)
, (<|)
, viewl
)
import qualified Data.Text as T
import qualified Graphics.Vty as V
import qualified Skylighting.Core as Sky
import Matterhorn.Constants ( normalChannelSigil, editMarking )
import Matterhorn.Draw.RichText.Flatten
import Matterhorn.Draw.RichText.Wrap
import Matterhorn.Themes
import Matterhorn.Types ( HighlightSet(..), emptyHSet, SemEq(..)
, addUserSigil, resultToWidget )
import Matterhorn.Types.RichText
-- | Render rich text.
renderRichText :: SemEq a
=> Text
-- ^ The username of the currently-authenticated user.
-> HighlightSet
-- ^ A highlight set for highlighting channel and
-- usernames and code blocks.
-> Maybe Int
-- ^ An optional maximum width.
-> Bool
-- ^ Whether to do line wrapping.
-> Maybe Int
-- ^ At what height to truncate long verbatim/code blocks
-> Maybe (Int -> Inline -> Maybe a)
-- ^ An optional function to build resource names for
-- clickable regions.
-> Blocks
-- ^ The content to render.
-> Widget a
renderRichText curUser hSet w doWrap doVerbTrunc nameGen (Blocks bs) =
runReader (do
blocks <- mapM renderBlock (addBlankLines bs)
return $ B.vBox $ toList blocks)
(DrawCfg { drawCurUser = curUser
, drawHighlightSet = hSet
, drawLineWidth = w
, drawDoLineWrapping = doWrap
, drawTruncateVerbatimBlocks = doVerbTrunc
, drawNameGen = nameGen
})
-- Render text to markdown without username highlighting, permalink
-- detection, or clickable links
renderText :: SemEq a => Text -> Widget a
renderText txt = renderText' Nothing "" emptyHSet Nothing txt
renderText' :: SemEq a
=> Maybe TeamBaseURL
-- ^ An optional base URL against which to match post links.
-> Text
-- ^ The username of the currently-authenticated user.
-> HighlightSet
-- ^ A highlight set for highlighting channel and usernames.
-> Maybe (Int -> Inline -> Maybe a)
-- ^ An optional function to build resource names for
-- clickable regions.
-> Text
-- ^ The text to parse and then render as rich text.
-> Widget a
renderText' baseUrl curUser hSet nameGen t =
renderRichText curUser hSet Nothing True Nothing nameGen $
parseMarkdown baseUrl t
-- Add blank lines only between adjacent elements of the same type, to
-- save space
addBlankLines :: Seq Block -> Seq Block
addBlankLines = go' . viewl
where go' EmptyL = S.empty
go' (x :< xs) = go x (viewl xs)
go a (b :< rs)
| sameBlockType a b = a <| blank <| go b (viewl rs)
| otherwise = a <| go b (viewl rs)
go x EmptyL = S.singleton x
blank = Para (Inlines $ S.singleton ESpace)
vBox :: F.Foldable f => f (Widget a) -> Widget a
vBox = B.vBox . toList
hBox :: F.Foldable f => f (Widget a) -> Widget a
hBox = B.hBox . toList
header :: Int -> Widget a
header n = B.txt (T.replicate n "#")
maybeHLimit :: Maybe Int -> Widget a -> Widget a
maybeHLimit Nothing w = w
maybeHLimit (Just i) w = hLimit i w
type M a b = Reader (DrawCfg b) a
data DrawCfg a =
DrawCfg { drawCurUser :: Text
, drawHighlightSet :: HighlightSet
, drawLineWidth :: Maybe Int
, drawDoLineWrapping :: Bool
, drawTruncateVerbatimBlocks :: Maybe Int
, drawNameGen :: Maybe (Int -> Inline -> Maybe a)
}
renderBlock :: (Ord a, SemEq a) => Block -> M (Widget a) a
renderBlock (Table aligns headings body) = do
headingWs <- mapM renderInlines headings
bodyWs <- forM body $ mapM renderInlines
let t = B.table (headingWs : bodyWs)
alignPairs = zip [0..] aligns
align (_, LeftAlignedCol) = id
align (_, DefaultAlignedCol) = id
align (i, RightAlignedCol) = B.alignRight i
align (i, CenterAlignedCol) = B.alignCenter i
applyAlignment = foldr (.) id (align <$> alignPairs)
return $ B.renderTable $ applyAlignment t
renderBlock (Para is) =
renderInlines is
renderBlock (Header n is) = do
headerTxt <- withReader (\c -> c { drawLineWidth = subtract 1 <$> drawLineWidth c }) $
renderInlines is
return $ B.withDefAttr clientHeaderAttr $
hBox [ B.padRight (B.Pad 1) $ header n
, headerTxt
]
renderBlock (Blockquote bs) = do
w <- asks drawLineWidth
bws <- mapM renderBlock (unBlocks bs)
return $ maybeHLimit w $ addQuoting $ vBox bws
renderBlock (List ty spacing bs) = do
w <- asks drawLineWidth
lst <- renderList ty spacing bs
return $ maybeHLimit w lst
renderBlock (CodeBlock ci tx) = do
hSet <- asks drawHighlightSet
let f = maybe renderRawCodeBlock
(renderCodeBlock (hSyntaxMap hSet))
mSyntax
mSyntax = do
lang <- codeBlockLanguage ci
Sky.lookupSyntax lang (hSyntaxMap hSet)
w <- f tx
trunc <- asks drawTruncateVerbatimBlocks
case trunc of
Nothing -> return w
Just maxHeight -> return $ maybeTruncVerbatim maxHeight w
renderBlock (HTMLBlock t) = do
w <- asks drawLineWidth
return $ maybeHLimit w $ textWithCursor t
renderBlock (HRule) = do
w <- asks drawLineWidth
return $ maybeHLimit w $ B.vLimit 1 (B.fill '*')
maybeTruncVerbatim :: Int -> B.Widget n -> B.Widget n
maybeTruncVerbatim maxHeight w =
Widget (B.hSize w) (B.vSize w) $ do
result <- render w
let h = V.imageHeight (result^.B.imageL)
if h > maxHeight
then render $ B.vBox [ B.vLimit maxHeight $ B.Widget B.Fixed B.Fixed $ return result
, B.withDefAttr verbatimTruncateMessageAttr $
B.str $ "(Showing " <> show maxHeight <> " of " <> show h <> " lines)"
]
else return result
quoteChar :: Char
quoteChar = '>'
addQuoting :: B.Widget n -> B.Widget n
addQuoting w =
B.Widget B.Fixed (B.vSize w) $ do
ctx <- B.getContext
childResult <- B.render $ B.hLimit (ctx^.B.availWidthL - 2) w
let quoteBorder = B.raw $ V.charFill (ctx^.B.attrL) quoteChar 1 height
height = V.imageHeight $ childResult^.B.imageL
B.render $ B.hBox [ B.padRight (B.Pad 1) quoteBorder
, B.Widget B.Fixed B.Fixed $ return childResult
]
renderCodeBlock :: Sky.SyntaxMap -> Sky.Syntax -> Text -> M (Widget a) b
renderCodeBlock syntaxMap syntax tx = do
let result = Sky.tokenize cfg syntax tx
cfg = Sky.TokenizerConfig syntaxMap False
case result of
Left _ -> renderRawCodeBlock tx
Right tokLines -> do
let padding = B.padLeftRight 1 (B.vLimit (length tokLines) B.vBorder)
return $ (B.txt $ "[" <> Sky.sName syntax <> "]") B.<=>
(padding <+> BS.renderRawSource textWithCursor tokLines)
renderRawCodeBlock :: Text -> M (Widget a) b
renderRawCodeBlock tx = do
doWrap <- asks drawDoLineWrapping
let hPolicy = if doWrap then Greedy else Fixed
return $ B.withDefAttr codeAttr $
Widget hPolicy Fixed $ do
c <- B.getContext
let theLines = expandEmpty <$> T.lines tx
expandEmpty "" = " "
expandEmpty s = s
wrapFunc = if doWrap then wrappedTextWithCursor
else textWithCursor
renderedText <- render (B.hLimit (c^.B.availWidthL - 3) $ B.vBox $
wrapFunc <$> theLines)
let textHeight = V.imageHeight $ renderedText^.imageL
padding = B.padLeftRight 1 (B.vLimit textHeight B.vBorder)
render $ padding <+> (resultToWidget renderedText)
renderInlines :: (Ord a, SemEq a) => Inlines -> M (Widget a) a
renderInlines es = do
w <- asks drawLineWidth
hSet <- asks drawHighlightSet
curUser <- asks drawCurUser
nameGen <- asks drawNameGen
return $ B.Widget B.Fixed B.Fixed $ do
ctx <- B.getContext
let width = fromMaybe (ctx^.B.availWidthL) w
ws = fmap (renderWrappedLine curUser) $
mconcat $
(doLineWrapping width <$> (F.toList $ flattenInlineSeq hSet nameGen es))
B.render (vBox ws)
renderList :: (Ord a, SemEq a) => ListType -> ListSpacing -> Seq Blocks -> M (Widget a) a
renderList ty _spacing bs = do
let is = case ty of
BulletList _ -> repeat ("• ")
OrderedList s _ Period ->
[ T.pack (show (n :: Int)) <> ". " | n <- [s..] ]
OrderedList s _ OneParen ->
[ T.pack (show (n :: Int)) <> ") " | n <- [s..] ]
OrderedList s _ TwoParens ->
[ T.pack (show (n :: Int)) <> ")) " | n <- [s..] ]
results <- forM (zip is $ unBlocks <$> (F.toList bs)) $ \(i, b) -> do
blocks <- mapM renderBlock b
return $ B.txt i <+> vBox blocks
return $ vBox results
renderWrappedLine :: (Ord a, Show a) => Text -> WrappedLine a -> Widget a
renderWrappedLine curUser l = hBox $ F.toList $ renderFlattenedValue curUser <$> l
renderFlattenedValue :: (Ord a, Show a) => Text -> FlattenedValue a -> Widget a
renderFlattenedValue curUser (NonBreaking rs) =
let renderLine = hBox . F.toList . fmap (renderFlattenedValue curUser)
in vBox (F.toList $ renderLine <$> F.toList rs)
renderFlattenedValue curUser (SingleInline fi) = addClickable $ addHyperlink $ addStyles widget
where
val = fiValue fi
mUrl = fiURL fi
mName = fiName fi
styles = fiStyles fi
addStyles w = foldr addStyle w styles
addStyle s =
B.withDefAttr $ case s of
Strong -> clientStrongAttr
Code -> codeAttr
Permalink -> permalinkAttr
Strikethrough -> strikeThroughAttr
Emph -> clientEmphAttr
addHyperlink = case mUrl of
Nothing -> id
Just u -> B.withDefAttr urlAttr . B.hyperlink (unURL u)
addClickable w = case mName of
Nothing -> id w
Just nm -> B.clickable nm w
widget = case val of
FSpace -> B.txt " "
FUser u -> colorUsername curUser u $ addUserSigil u
FChannel c -> B.withDefAttr channelNameAttr $
B.txt $ normalChannelSigil <> c
FEmoji em -> B.withDefAttr emojiAttr $
B.txt $ ":" <> em <> ":"
FText t -> if t == T.singleton (cursorSentinel)
then B.visible $ B.txt " "
else textWithCursor t
FEditSentinel recent -> let attr = if recent
then editedRecentlyMarkingAttr
else editedMarkingAttr
in B.withDefAttr attr $ B.txt editMarking
textWithCursor :: Text -> Widget a
textWithCursor t
| T.any (== cursorSentinel) t = B.visible $ B.txt $ removeCursor t
| otherwise = B.txt t
wrappedTextWithCursor :: Text -> Widget a
wrappedTextWithCursor t
| T.any (== cursorSentinel) t = B.visible $ B.txtWrap $ removeCursor t
| otherwise = B.txtWrap t
removeCursor :: Text -> Text
removeCursor = T.filter (/= cursorSentinel)
-- Cursor sentinel for tracking the user's cursor position in previews.
cursorSentinel :: Char
cursorSentinel = '‸'