patat-0.15.2.0: lib/Patat/Presentation/Display.hs
--------------------------------------------------------------------------------
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Patat.Presentation.Display
( Display (..)
, displayPresentation
, displayPresentationError
, dumpPresentation
) where
--------------------------------------------------------------------------------
import Control.Monad (guard)
import Control.Monad.Identity (runIdentity)
import Control.Monad.Writer (Writer, execWriter, tell)
import qualified Data.Aeson.Extended as A
import Data.Char.WCWidth.Extended (wcstrwidth)
import Data.Foldable (for_)
import qualified Data.HashMap.Strict as HMS
import qualified Data.List as L
import Data.Maybe (fromMaybe, maybeToList)
import qualified Data.Sequence.Extended as Seq
import qualified Data.Text as T
import Patat.Presentation.Display.CodeBlock
import Patat.Presentation.Display.Internal
import Patat.Presentation.Display.Table
import Patat.Presentation.Internal
import Patat.Presentation.Settings
import qualified Patat.Presentation.SpeakerNotes as SpeakerNotes
import Patat.Presentation.Syntax
import Patat.PrettyPrint ((<$$>))
import qualified Patat.PrettyPrint as PP
import Patat.Size
import Patat.Theme (Theme (..))
import qualified Patat.Theme as Theme
import Prelude
import qualified Text.Pandoc.Extended as Pandoc
--------------------------------------------------------------------------------
data Display = DisplayDoc PP.Doc | DisplayImage FilePath deriving (Show)
--------------------------------------------------------------------------------
-- | Display something within the presentation borders that draw the title and
-- the active slide number and so on.
displayWithBorders
:: Size -> Presentation -> (DisplaySettings -> PP.Doc) -> PP.Doc
displayWithBorders (Size rows columns) pres@Presentation {..} f =
(if null title
then mempty
else
let titleRemainder = columns - titleWidth - titleOffset
wrappedTitle = PP.spaces titleOffset <> PP.string title <> PP.spaces titleRemainder in
borders wrappedTitle <> PP.hardline) <>
f ds <> PP.hardline <>
PP.goToLine (rows - 2) <>
borders (PP.space <> PP.string author <> middleSpaces <> PP.string active <> PP.space) <>
PP.hardline
where
-- Get terminal width/title
settings = activeSettings pres
(sidx, _) = pActiveFragment
ds = DisplaySettings
{ dsSize = canvasSize
, dsMargins = margins settings
, dsWrap = fromMaybe NoWrap $ psWrap settings
, dsTabStop = maybe 4 A.unFlexibleNum $ psTabStop settings
, dsOSC8 = fromMaybe False (psLinks settings >>= lsOSC8)
, dsTheme = fromMaybe Theme.defaultTheme (psTheme settings)
, dsSyntaxMap = pSyntaxMap
, dsResolve = \var -> fromMaybe [] $ HMS.lookup var pVars
, dsRevealState = revealState
}
revealState = case activeFragment pres of
Just (ActiveContent _ _ c) -> c
_ -> mempty
-- Compute title.
breadcrumbs = fromMaybe [] $ Seq.safeIndex pBreadcrumbs sidx
plainTitle = PP.toString $ prettyInlines ds pTitle
breadTitle = mappend plainTitle $ mconcat
[ s
| b <- map (prettyInlines ds . snd) breadcrumbs
, s <- [" > ", PP.toString b]
]
title
| not . fromMaybe True $ psBreadcrumbs settings = plainTitle
| wcstrwidth breadTitle > columns = plainTitle
| otherwise = breadTitle
-- Dimensions of title.
titleWidth = wcstrwidth title
titleOffset = (columns - titleWidth) `div` 2
borders = themed ds themeBorders
-- Room left for content
canvasSize = Size (rows - 3) columns
-- Compute footer.
active
| fromMaybe True $ psSlideNumber settings = show (sidx + 1) ++ " / " ++ show (length pSlides)
| otherwise = ""
activeWidth = wcstrwidth active
author = PP.toString (prettyInlines ds pAuthor)
authorWidth = wcstrwidth author
middleSpaces = PP.spaces $ columns - activeWidth - authorWidth - 2
--------------------------------------------------------------------------------
displayPresentation :: Size -> Presentation -> Display
displayPresentation size pres@Presentation {..} =
case activeFragment pres of
Nothing -> DisplayDoc $ displayWithBorders size pres mempty
Just (ActiveContent fragment _ _)
| Just _ <- psImages pSettings
, Just image <- onlyImage fragment ->
DisplayImage $ T.unpack image
Just (ActiveContent fragment _ _) -> DisplayDoc $
displayWithBorders size pres $ \theme ->
prettyMargins theme fragment
Just (ActiveTitle block) -> DisplayDoc $
displayWithBorders size pres $ \ds ->
let auto = Margins {mTop = Auto, mRight = Auto, mLeft = Auto} in
prettyMargins ds {dsMargins = auto} [block]
where
-- Check if the fragment consists of "just a single image". Discard
-- headers.
onlyImage (Header{} : bs) = onlyImage bs
onlyImage bs = case bs of
[Figure _ bs'] -> onlyImage bs'
[Para [Image _ _ (target, _)]] -> Just target
_ -> Nothing
--------------------------------------------------------------------------------
-- | Displays an error in the place of the presentation. This is useful if we
-- want to display an error but keep the presentation running.
displayPresentationError :: Size -> Presentation -> String -> PP.Doc
displayPresentationError size pres err = displayWithBorders size pres $ \ds ->
themed ds themeStrong "Error occurred in the presentation:" <$$>
"" <$$>
(PP.string err)
--------------------------------------------------------------------------------
dumpPresentation :: Presentation -> IO ()
dumpPresentation pres@Presentation {..} =
PP.putDoc $ PP.removeControls $
PP.vcat $ L.intercalate ["{slide}"] $
map dumpSlide [0 .. length pSlides - 1]
where
dumpSlide :: Int -> [PP.Doc]
dumpSlide i = do
slide <- maybeToList $ getSlide i pres
dumpSpeakerNotes slide <> L.intercalate ["{fragment}"]
[ dumpFragment (i, j)
| j <- [0 .. numFragments slide - 1]
]
dumpSpeakerNotes :: Slide -> [PP.Doc]
dumpSpeakerNotes slide = do
guard (slideSpeakerNotes slide /= mempty)
pure $ PP.text $ "{speakerNotes: " <>
SpeakerNotes.toText (slideSpeakerNotes slide) <> "}"
dumpFragment :: Index -> [PP.Doc]
dumpFragment idx =
case displayPresentation (getSize idx) pres {pActiveFragment = idx} of
DisplayDoc doc -> [doc]
DisplayImage filepath -> [PP.string $ "{image: " ++ filepath ++ "}"]
getSize :: Index -> Size
getSize idx =
let settings = activeSettings pres {pActiveFragment = idx}
sRows = fromMaybe 24 $ A.unFlexibleNum <$> psRows settings
sCols = fromMaybe 72 $ A.unFlexibleNum <$> psColumns settings in
Size {..}
--------------------------------------------------------------------------------
-- | Renders the given blocks, adding margins based on the settings and wrapping
-- based on width.
prettyMargins :: DisplaySettings -> [Block] -> PP.Doc
prettyMargins ds blocks = vertical $
map horizontal blocks ++
case prettyReferences ds blocks of
[] -> []
refs ->
let doc0 = PP.vcat refs
size@(r, _) = PP.dimensions doc0 in
[(horizontalIndent size gmargins $ horizontalWrap gmargins doc0, r)]
where
Size rows columns = dsSize ds
gmargins = dsMargins ds
-- For every block, calculate the size based on its last fragment.
blockSize block =
let revealState = blocksRevealLastStep [block]
bmargins = marginsFor block
in PP.dimensions $ deindent bmargins $ horizontalWrap bmargins $
prettyBlock ds {dsRevealState = revealState} block
-- Vertically align some blocks by adding spaces in front of it.
-- We also take in the number of rows for every block so we don't
-- need to recompute it.
vertical :: [(PP.Doc, Int)] -> PP.Doc
vertical docs0 = mconcat (replicate top PP.hardline) <> doc
where
top = case mTop gmargins of
Auto -> (rows - actual) `div` 2
NotAuto x -> x
docs1 = [verticalPad r d | (d, r) <- docs0]
actual = sum $ L.intersperse 1 $ map snd docs1
doc = PP.vcat $ map fst docs1
-- Vertically pad a doc by adding lines below it.
-- Return the actual size as well as the padded doc.
verticalPad :: Int -> PP.Doc -> (PP.Doc, Int)
verticalPad desired doc0
| actual >= rows = (doc0, actual)
| otherwise = (doc0 <> padding, desired)
where
(actual, _) = PP.dimensions doc0
padding = mconcat $ replicate (desired - actual) PP.hardline
-- Render and horizontally align a block. Also returns the desired rows.
horizontal :: Block -> (PP.Doc, Int)
horizontal b@(Reveal ConcatWrapper reveal) =
-- Horizontally aligning a fragment with a ConcatWrapper is a special
-- case, as we want to horizontal align all the things inside
-- individually.
let (fblocks, _) = unzip $ map horizontal $
revealToBlocks (dsRevealState ds) ConcatWrapper reveal in
(PP.vcat fblocks, fst (blockSize b))
horizontal block =
let size@(r, _) = blockSize block
bmargins = marginsFor block in
( horizontalIndent size bmargins $ horizontalWrap bmargins $
prettyBlock ds block
, r
)
horizontalIndent :: (Int, Int) -> Margins -> PP.Doc -> PP.Doc
horizontalIndent (_, dcols) dmargins@Margins {..} doc0 =
PP.indent indentation indentation doc1
where
doc1 = deindent dmargins doc0
left = case mLeft of
NotAuto x -> x
Auto -> case mRight of
NotAuto _ -> 0
Auto -> (columns - dcols) `div` 2
indentation = PP.Indentation left mempty
-- Strip leading spaces to horizontally align code blocks etc.
deindent Margins {..} doc0 = case (mLeft, mRight) of
(Auto, Auto) -> PP.deindent doc0
_ -> doc0
-- Rearranges lines to fit into the wrap settings.
horizontalWrap :: Margins -> PP.Doc -> PP.Doc
horizontalWrap Margins {..} doc0 = case dsWrap ds of
NoWrap -> doc0
AutoWrap -> PP.wrapAt (Just $ columns - right - left) doc0
WrapAt col -> PP.wrapAt (Just col) doc0
where
right = case mRight of
Auto -> 0
NotAuto x -> x
left = case mLeft of
Auto -> 0
NotAuto x -> x
-- Find the right margins for the specific block. Currently, only headers
-- can have different margins.
marginsFor :: Block -> Margins
marginsFor (Header n _ _) = fromMaybe gmargins $ do
align <- Theme.htAlign $ Theme.themeForHeader n (dsTheme ds)
guard $ align == Theme.CenterHeaderAlign
pure gmargins {mLeft = Auto, mRight = Auto}
marginsFor _ = gmargins
--------------------------------------------------------------------------------
prettyBlock :: DisplaySettings -> Block -> PP.Doc
prettyBlock ds (Plain inlines) = prettyInlines ds inlines
prettyBlock ds (Para inlines) =
prettyInlines ds inlines <> PP.hardline
prettyBlock ds (Header n _ inlines) =
themed ds style content <> PP.hardline <>
(case underline of
Just t | t /= "" ->
themed ds style (PP.string $ take cols $ cycle $ T.unpack t) <>
PP.hardline
_ -> mempty)
where
prefix = fromMaybe mempty $ Theme.htPrefix headerTheme
content = PP.text prefix <> prettyInlines ds inlines
(_, cols) = PP.dimensions content
underline = Theme.htUnderline headerTheme
headerTheme = Theme.themeForHeader n (dsTheme ds)
style _ = Theme.htStyle headerTheme
prettyBlock ds (CodeBlock classes txt) =
prettyCodeBlock ds classes txt
prettyBlock ds (BulletList bss) = PP.vcat
[ PP.indent
(PP.Indentation 2 $ themed ds themeBulletList prefix)
(PP.Indentation 4 mempty)
(prettyBlocks ds' bs)
| bs <- bss
] <> PP.hardline
where
prefix = PP.string [marker] <> " "
marker = case T.unpack <$> themeBulletListMarkers theme of
Just (x : _) -> x
_ -> '-'
-- Cycle the markers.
theme = dsTheme ds
theme' = theme
{ themeBulletListMarkers =
(\ls -> T.drop 1 ls <> T.take 1 ls) <$> themeBulletListMarkers theme
}
ds' = ds {dsTheme = theme'}
prettyBlock ds (OrderedList _ bss) = PP.vcat
[ PP.indent
(PP.Indentation 0 $ themed ds themeOrderedList $ PP.string prefix)
(PP.Indentation 4 mempty)
(prettyBlocks ds bs)
| (prefix, bs) <- zip padded bss
] <> PP.hardline
where
padded = [n ++ replicate (4 - length n) ' ' | n <- numbers]
numbers =
[ show i ++ "."
| i <- [1 .. length bss]
]
prettyBlock _ds (RawBlock _ t) = PP.text t <> PP.hardline
prettyBlock _ds HorizontalRule = "---"
prettyBlock ds (BlockQuote bs) =
let quote = PP.Indentation 0 (themed ds themeBlockQuote "> ") in
PP.indent quote quote (themed ds themeBlockQuote $ prettyBlocks ds bs)
prettyBlock ds (DefinitionList terms) =
PP.vcat $ map prettyDefinition terms
where
prettyDefinition (term, definitions) =
themed ds themeDefinitionTerm (prettyInlines ds term) <$$>
PP.hardline <> PP.vcat
[ PP.indent
(PP.Indentation 0 (themed ds themeDefinitionList ": "))
(PP.Indentation 4 mempty) $
prettyBlocks ds (plainToPara definition)
| definition <- definitions
]
plainToPara :: [Block] -> [Block]
plainToPara = map $ \case
Plain inlines -> Para inlines
block -> block
prettyBlock ds (Table caption aligns headers rows) =
PP.wrapAt Nothing $
prettyTableDisplay ds TableDisplay
{ tdCaption = prettyInlines ds caption
, tdAligns = map align aligns
, tdHeaders = map (prettyBlocks ds) headers
, tdRows = map (map (prettyBlocks ds)) rows
}
where
align Pandoc.AlignLeft = PP.AlignLeft
align Pandoc.AlignCenter = PP.AlignCenter
align Pandoc.AlignDefault = PP.AlignLeft
align Pandoc.AlignRight = PP.AlignRight
prettyBlock ds (Div _attrs blocks) = prettyBlocks ds blocks
prettyBlock ds (LineBlock inliness) =
let ind = PP.Indentation 0 (themed ds themeLineBlock "| ") in
PP.wrapAt Nothing $
PP.indent ind ind $
PP.vcat $
map (prettyInlines ds) inliness
prettyBlock ds (Figure _attr blocks) = prettyBlocks ds blocks
prettyBlock ds (Reveal w fragment) = prettyBlocks ds $
revealToBlocks (dsRevealState ds) w fragment
prettyBlock ds (VarBlock var) = prettyBlocks ds $ dsResolve ds var
prettyBlock _ (SpeakerNote _) = mempty
prettyBlock _ (Config _) = mempty
--------------------------------------------------------------------------------
prettyBlocks :: DisplaySettings -> [Block] -> PP.Doc
prettyBlocks ds = PP.vcat . map (prettyBlock ds)
--------------------------------------------------------------------------------
prettyInline :: DisplaySettings -> Inline -> PP.Doc
prettyInline _ds Space = PP.space
prettyInline _ds (Str str) = PP.text str
prettyInline ds (Emph inlines) =
themed ds themeEmph $
prettyInlines ds inlines
prettyInline ds (Strong inlines) =
themed ds themeStrong $
prettyInlines ds inlines
prettyInline ds (Underline inlines) =
themed ds themeUnderline $
prettyInlines ds inlines
prettyInline ds (Code _ txt) =
themed ds themeCode $
PP.text (" " <> txt <> " ")
prettyInline ds link@(Link _attrs _text (target, _title))
| Just (text, _, _) <- toReferenceLink link =
let doc = Just $ prettyInlines ds text in
"[" <> themed ds themeLinkText (hyperlink ds target doc) <> "]"
| otherwise =
"<" <> themed ds themeLinkTarget (hyperlink ds target Nothing) <> ">"
prettyInline _ds SoftBreak = PP.softline
prettyInline _ds LineBreak = PP.hardline
prettyInline ds (Strikeout t) =
"~~" <> themed ds themeStrikeout (prettyInlines ds t) <> "~~"
prettyInline ds (Quoted Pandoc.SingleQuote t) =
"'" <> themed ds themeQuoted (prettyInlines ds t) <> "'"
prettyInline ds (Quoted Pandoc.DoubleQuote t) =
"'" <> themed ds themeQuoted (prettyInlines ds t) <> "'"
prettyInline ds (Math _ t) =
themed ds themeMath (PP.text t)
prettyInline ds (Image _attrs text (target, _title)) =
" <> ")"
prettyInline _ (RawInline _ t) = PP.text t
-- These elements aren't really supported.
prettyInline ds (Cite _ t) = prettyInlines ds t
prettyInline ds (Span _ t) = prettyInlines ds t
prettyInline _ (Note _) = mempty -- TODO: support notes?
prettyInline ds (Superscript t) = prettyInlines ds t
prettyInline ds (Subscript t) = prettyInlines ds t
prettyInline ds (SmallCaps t) = prettyInlines ds t
-- prettyInline unsupported = PP.ondullred $ PP.string $ show unsupported
--------------------------------------------------------------------------------
prettyInlines :: DisplaySettings -> [Inline] -> PP.Doc
prettyInlines ds = mconcat . map (prettyInline ds)
--------------------------------------------------------------------------------
type Reference = ([Inline], T.Text, T.Text)
--------------------------------------------------------------------------------
prettyReferences :: DisplaySettings -> [Block] -> [PP.Doc]
prettyReferences ds =
map prettyReference . execWriter . dftBlocks (pure . pure) tellReference
where
tellReference :: Inline -> Writer [Reference] [Inline]
tellReference inline = do
for_ (toReferenceLink inline) (tell . pure)
pure [inline]
prettyReference :: Reference -> PP.Doc
prettyReference (text, target, title) =
"[" <>
themed ds themeLinkText
(prettyInlines ds $ newlineToSpace text) <>
"]: " <>
themed ds themeLinkTarget (PP.text target) <>
(if T.null title
then mempty
else PP.space <> PP.text title)
newlineToSpace :: [Inline] -> [Inline]
newlineToSpace = runIdentity . dftInlines (pure . pure) work
where
work x = pure $ case x of
SoftBreak -> [Space]
LineBreak -> [Space]
_ -> [x]
--------------------------------------------------------------------------------
toReferenceLink :: Inline -> Maybe Reference
toReferenceLink (Link _attrs text (target, title))
| [Str target] /= text = Just (text, target, title)
toReferenceLink _ = Nothing
--------------------------------------------------------------------------------
hyperlink :: DisplaySettings -> T.Text -> Maybe PP.Doc -> PP.Doc
hyperlink ds url Nothing
| dsOSC8 ds = PP.hyperlink (T.unpack url) (PP.text url)
| otherwise = PP.text url
hyperlink ds url (Just doc)
| dsOSC8 ds = PP.hyperlink (T.unpack url) doc
| otherwise = doc