patat-0.12.0.0: lib/Patat/Presentation/Display.hs
--------------------------------------------------------------------------------
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Patat.Presentation.Display
( Display (..)
, displayPresentation
, displayPresentationError
, dumpPresentation
) where
--------------------------------------------------------------------------------
import Control.Monad (guard)
import qualified Data.Aeson.Extended as A
import Data.Char.WCWidth.Extended (wcstrwidth)
import Data.Data.Extended (grecQ)
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 qualified Patat.Presentation.Comments as Comments
import Patat.Presentation.Display.CodeBlock
import Patat.Presentation.Display.Internal
import Patat.Presentation.Display.Table
import Patat.Presentation.Internal
import Patat.Presentation.Settings
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
import qualified Text.Pandoc.Writers.Shared 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
(sidx, _) = pActiveFragment
settings = activeSettings pres
ds = DisplaySettings
{ dsSize = canvasSize
, dsMargins = margins settings
, dsWrap = fromMaybe NoWrap $ psWrap settings
, dsTabStop = maybe 4 A.unFlexibleNum $ psTabStop settings
, dsTheme = fromMaybe Theme.defaultTheme (psTheme settings)
, dsSyntaxMap = pSyntaxMap
}
-- 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 ->
prettyFragment theme fragment
Just (ActiveTitle block) -> DisplayDoc $
displayWithBorders size pres $ \ds ->
let auto = Margins {mTop = Auto, mRight = Auto, mLeft = Auto} in
prettyFragment ds {dsMargins = auto} $ Fragment [block]
where
-- Check if the fragment consists of "just a single image". Discard
-- headers.
onlyImage (Fragment (Pandoc.Header{} : bs)) = onlyImage (Fragment bs)
onlyImage (Fragment bs) = case bs of
[Pandoc.Figure _ _ bs'] -> onlyImage (Fragment bs')
[Pandoc.Para [Pandoc.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
dumpComment slide <> L.intercalate ["{fragment}"]
[ dumpFragment (i, j)
| j <- [0 .. numFragments slide - 1]
]
dumpComment :: Slide -> [PP.Doc]
dumpComment slide = do
guard (Comments.cSpeakerNotes comment /= mempty)
pure $ PP.text $ "{speakerNotes: " <>
Comments.speakerNotesToText (Comments.cSpeakerNotes comment) <> "}"
where
comment = slideComment 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 {..}
--------------------------------------------------------------------------------
prettyFragment :: DisplaySettings -> Fragment -> PP.Doc
prettyFragment ds (Fragment blocks) = vertical $
PP.vcat (map (horizontal . prettyBlock ds) blocks) <>
case prettyReferences ds blocks of
[] -> mempty
refs -> PP.hardline <> PP.vcat (map horizontal refs)
where
Size rows columns = dsSize ds
Margins {..} = dsMargins ds
vertical doc0 =
mconcat (replicate top PP.hardline) <> doc0
where
top = case mTop of
Auto -> let (r, _) = PP.dimensions doc0 in (rows - r) `div` 2
NotAuto x -> x
horizontal = horizontalIndent . horizontalWrap
horizontalIndent doc0 = PP.indent indentation indentation doc1
where
doc1 = case (mLeft, mRight) of
(Auto, Auto) -> PP.deindent doc0
_ -> doc0
(_, dcols) = PP.dimensions doc1
left = case mLeft of
NotAuto x -> x
Auto -> case mRight of
NotAuto _ -> 0
Auto -> (columns - dcols) `div` 2
indentation = PP.Indentation left mempty
horizontalWrap 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
--------------------------------------------------------------------------------
prettyBlock :: DisplaySettings -> Pandoc.Block -> PP.Doc
prettyBlock ds (Pandoc.Plain inlines) = prettyInlines ds inlines
prettyBlock ds (Pandoc.Para inlines) =
prettyInlines ds inlines <> PP.hardline
prettyBlock ds (Pandoc.Header i _ inlines) =
themed ds themeHeader (PP.string (replicate i '#') <+> prettyInlines ds inlines) <>
PP.hardline
prettyBlock ds (Pandoc.CodeBlock (_, classes, _) txt) =
prettyCodeBlock ds classes txt
prettyBlock ds (Pandoc.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 (Pandoc.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 (Pandoc.RawBlock _ t) = PP.text t <> PP.hardline
prettyBlock _ds Pandoc.HorizontalRule = "---"
prettyBlock ds (Pandoc.BlockQuote bs) =
let quote = PP.Indentation 0 (themed ds themeBlockQuote "> ") in
PP.indent quote quote (themed ds themeBlockQuote $ prettyBlocks ds bs)
prettyBlock ds (Pandoc.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 (Pandoc.plainToPara definition)
| definition <- definitions
]
prettyBlock ds (Pandoc.Table _ caption specs thead tbodies tfoot) =
PP.wrapAt Nothing $
prettyTable ds Table
{ tCaption = prettyInlines ds caption'
, tAligns = map align aligns
, tHeaders = map (prettyBlocks ds) headers
, tRows = map (map (prettyBlocks ds)) rows
}
where
(caption', aligns, _, headers, rows) = Pandoc.toLegacyTable
caption specs thead tbodies tfoot
align Pandoc.AlignLeft = PP.AlignLeft
align Pandoc.AlignCenter = PP.AlignCenter
align Pandoc.AlignDefault = PP.AlignLeft
align Pandoc.AlignRight = PP.AlignRight
prettyBlock ds (Pandoc.Div _attrs blocks) = prettyBlocks ds blocks
prettyBlock ds (Pandoc.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 (Pandoc.Figure _attr _caption blocks) =
prettyBlocks ds blocks
--------------------------------------------------------------------------------
prettyBlocks :: DisplaySettings -> [Pandoc.Block] -> PP.Doc
prettyBlocks ds = PP.vcat . map (prettyBlock ds)
--------------------------------------------------------------------------------
prettyInline :: DisplaySettings -> Pandoc.Inline -> PP.Doc
prettyInline _ds Pandoc.Space = PP.space
prettyInline _ds (Pandoc.Str str) = PP.text str
prettyInline ds (Pandoc.Emph inlines) =
themed ds themeEmph $
prettyInlines ds inlines
prettyInline ds (Pandoc.Strong inlines) =
themed ds themeStrong $
prettyInlines ds inlines
prettyInline ds (Pandoc.Underline inlines) =
themed ds themeUnderline $
prettyInlines ds inlines
prettyInline ds (Pandoc.Code _ txt) =
themed ds themeCode $
PP.text (" " <> txt <> " ")
prettyInline ds link@(Pandoc.Link _attrs text (target, _title))
| isReferenceLink link =
"[" <> themed ds themeLinkText (prettyInlines ds text) <> "]"
| otherwise =
"<" <> themed ds themeLinkTarget (PP.text target) <> ">"
prettyInline _ds Pandoc.SoftBreak = PP.softline
prettyInline _ds Pandoc.LineBreak = PP.hardline
prettyInline ds (Pandoc.Strikeout t) =
"~~" <> themed ds themeStrikeout (prettyInlines ds t) <> "~~"
prettyInline ds (Pandoc.Quoted Pandoc.SingleQuote t) =
"'" <> themed ds themeQuoted (prettyInlines ds t) <> "'"
prettyInline ds (Pandoc.Quoted Pandoc.DoubleQuote t) =
"'" <> themed ds themeQuoted (prettyInlines ds t) <> "'"
prettyInline ds (Pandoc.Math _ t) =
themed ds themeMath (PP.text t)
prettyInline ds (Pandoc.Image _attrs text (target, _title)) =
" <> ")"
prettyInline _ (Pandoc.RawInline _ t) = PP.text t
-- These elements aren't really supported.
prettyInline ds (Pandoc.Cite _ t) = prettyInlines ds t
prettyInline ds (Pandoc.Span _ t) = prettyInlines ds t
prettyInline ds (Pandoc.Note t) = prettyBlocks ds t
prettyInline ds (Pandoc.Superscript t) = prettyInlines ds t
prettyInline ds (Pandoc.Subscript t) = prettyInlines ds t
prettyInline ds (Pandoc.SmallCaps t) = prettyInlines ds t
-- prettyInline unsupported = PP.ondullred $ PP.string $ show unsupported
--------------------------------------------------------------------------------
prettyInlines :: DisplaySettings -> [Pandoc.Inline] -> PP.Doc
prettyInlines ds = mconcat . map (prettyInline ds)
--------------------------------------------------------------------------------
prettyReferences :: DisplaySettings -> [Pandoc.Block] -> [PP.Doc]
prettyReferences ds =
map prettyReference . getReferences
where
getReferences :: [Pandoc.Block] -> [Pandoc.Inline]
getReferences = filter isReferenceLink . grecQ
prettyReference :: Pandoc.Inline -> PP.Doc
prettyReference (Pandoc.Link _attrs text (target, title)) =
"[" <>
themed ds themeLinkText
(prettyInlines ds $ Pandoc.newlineToSpace text) <>
"](" <>
themed ds themeLinkTarget (PP.text target) <>
(if T.null title
then mempty
else PP.space <> "\"" <> PP.text title <> "\"")
<> ")"
prettyReference _ = mempty
--------------------------------------------------------------------------------
isReferenceLink :: Pandoc.Inline -> Bool
isReferenceLink (Pandoc.Link _attrs text (target, _)) =
[Pandoc.Str target] /= text
isReferenceLink _ = False