aboutsummaryrefslogtreecommitdiffstats
path: root/app/Render.hs
blob: 2220fb8156936a62621f8b76914a920a1ed080b8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE UndecidableInstances #-}

-- | This module is an experimental wrapper around the prettyprinter module.
module Render
  ( -- * Re-exports
    module Prettyprinter,
    module Prettyprinter.Render.Terminal,

    -- * Render
    Render (..),
    (<<<),
    styled,

    -- * Reporting styles
    Detailed (..),
    Summarized (..),
    Linked (..),

    -- * Markdown
    Markdown (..),
    renderAsMarkdown,

    -- * Additional symbols
    plus,
    minus,
  )
where

import CMark qualified as D
import Data.List (isPrefixOf, isSuffixOf, intersperse)
import Data.Maybe (catMaybes)
import Data.Text qualified as T
import Data.Time.Calendar (Day)
import Prettyprinter
import Prettyprinter.Render.Terminal

-- | The render class is a superclass of `Pretty`. It exists to facilitate
-- reporting styles (see below), as well as decomposing aggregate entities into composable parts (for instance `IssueTitle`, `IssueTags` of `Issue`).
--
-- Renderable entities are expected to define `Render (Detailed a)` and default `Render a` to that instance.
class Render a where
  render :: a -> Doc AnsiStyle
  default render :: Pretty a => a -> Doc AnsiStyle
  render = pretty

instance Render (Doc AnsiStyle) where
  render = id

instance Render T.Text

instance Render String

instance Render Day where
  render = render . show

-- | The `(<<<)` combinator concatenates renderables. It takes care of inserting spaces between non-empty renderables automatically, obsoleting prettyprinter's `(<+>)` and `(<>)`.
(<<<) :: (Render a, Render b) => a -> b -> Doc AnsiStyle
(<<<) a' b' =
  case (nonEmpty a', nonEmpty b') of
    (Nothing, Nothing) -> emptyDoc
    (Just a, Nothing) -> a
    (Nothing, Just b) -> b
    (Just a, Just b) ->
      if endsWithNL a || startsWithNL b
        then a <> b
        else a <> space <> b
  where
    nonEmpty x' =
      let x = render x'
       in if not (null (show x)) then Just (render x) else Nothing
    startsWithNL = ("\n" `isPrefixOf`) . show . render
    endsWithNL = ("\n" `isSuffixOf`) . show . render

-- | `styled` allows to annotate a document with multiple annotations. It obsoletes prettyprinter's `annotate`.
styled :: [AnsiStyle] -> Doc AnsiStyle -> Doc AnsiStyle
styled [] doc = doc
styled as doc = foldl1 (.) (map annotate as) $ doc

-- | The detailed report should present all information, corresponding to the semantics of the `show` command.
--
-- It should be the default instance for all data types.
newtype Detailed a = Detailed {unDetailed :: a}

-- | The summarized report should present most relevant information in a concise way, corresponding to the semantics of the `list` command.
newtype Summarized a = Summarized {unSummarized :: a}

-- | The linked report should present strictly necessary information. It should be used when one entity refers to another.
newtype Linked a = Linked {unLinked :: a}

newtype Markdown = Markdown {unMarkdown :: D.Node}

renderAsMarkdown :: T.Text -> Doc AnsiStyle
renderAsMarkdown = render . Markdown . D.commonmarkToNode []

instance Render Markdown where
  render = maybe emptyDoc go . rec . unMarkdown
    where
      rec (D.Node _ D.SOFTBREAK _) = Nothing
      rec (D.Node p t ns) = Just $ D.Node p t (catMaybes $ map rec ns)

      go (D.Node _ D.DOCUMENT ns) = vsep (intersperse (pretty ("" :: T.Text)) (map go ns))
      go (D.Node _ D.THEMATIC_BREAK _) = pretty ("***" :: T.Text)
      go (D.Node _ D.PARAGRAPH ns) = fillSep $ map go ns
      go (D.Node _ D.BLOCK_QUOTE ns) =
        styled [color Black] . fillSep $
          pretty (">" :: T.Text)
            : [ align . fillSep $
                  map go ns
                    ++ [pretty ("\n" :: T.Text)]
              ]
      go (D.Node _ (D.HTML_BLOCK s) _) =
        styled [color Yellow] $
          hsep
            [ pretty (T.strip s),
              pretty ("\n" :: T.Text)
            ]
      go (D.Node _ (D.CUSTOM_BLOCK _ _) ns) =
        styled [color Yellow] $
          hsep $
            map go ns ++ [pretty ("\n" :: T.Text)]
      go (D.Node _ (D.CODE_BLOCK _ s) _) =
        hcat
          [ styled [color Green] (pretty ("────────────────────\n" :: T.Text)),
            styled [color Yellow]
              . vsep
              . map (styled [color Yellow] . pretty)
              $ T.lines s,
            pretty ("\n" :: T.Text),
            styled [color Green] (pretty ("────────────────────" :: T.Text))
          ]
      go (D.Node _ (D.HEADING c) ns) =
        styled [color Magenta] . fillSep $
          pretty (T.replicate c "#") : map go ns
      go (D.Node _ (D.LIST _) ns) =
        hsep (map ((pretty ("- " :: T.Text)) <>) (map go ns))
      go (D.Node _ D.ITEM ns) = fillSep (map go ns)
      go (D.Node _ (D.TEXT s) _) =
        fillSep . map (pretty . T.strip) $
          T.words s
      go (D.Node _ D.LINEBREAK _) = hardline
      go (D.Node _ (D.HTML_INLINE s) _) =
        styled [color Yellow] $ pretty (T.strip s)
      go (D.Node _ (D.CUSTOM_INLINE _ _) ns) =
        styled [color Yellow] . fillSep $ map go ns
      go (D.Node _ (D.CODE s) _) =
        styled [color Yellow] $ pretty (T.strip s)
      go (D.Node _ D.EMPH ns) = styled [italicized] $ fillSep (map go ns)
      go (D.Node _ D.STRONG ns) = styled [bold] $ fillSep (map go ns)
      go (D.Node _ D.SOFTBREAK _) = emptyDoc
      go (D.Node _ (D.LINK url "") ns) =
        styled [color Blue] $
          hcat
            [ pretty ("[" :: T.Text),
              hsep (map go ns),
              pretty ("](" :: T.Text),
              pretty url,
              pretty (")" :: T.Text)
            ]
      go (D.Node _ (D.LINK url title) ns) =
        styled [color Blue] $
          hcat
            [ pretty ("[" :: T.Text),
              hsep (map go ns),
              pretty ("](" :: T.Text),
              pretty url,
              pretty (" \"" :: T.Text),
              pretty title,
              pretty ("\")" :: T.Text)
            ]
      go (D.Node _ (D.IMAGE url "") ns) =
        styled [color Blue] $
          hcat
            [ pretty ("![" :: T.Text),
              hsep (map go ns),
              pretty ("](" :: T.Text),
              pretty url,
              pretty (")" :: T.Text)
            ]
      go (D.Node _ (D.IMAGE url title) ns) =
        styled [color Blue] $
          hcat
            [ pretty ("![" :: T.Text),
              hsep (map go ns),
              pretty ("](" :: T.Text),
              pretty url,
              pretty (" \"" :: T.Text),
              pretty title,
              pretty ("\")" :: T.Text)
            ]

plus :: Doc ann
plus = pretty ("+" :: T.Text)

minus :: Doc ann
minus = pretty ("-" :: T.Text)