packages feed

tadka-2.0.0.0: src/Tadka/Internal/Render.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE TypeFamilies #-}


-- No compatibility guarantee.
module Tadka.Internal.Render
  ( Output
  , Renderer (..)
  , SomeRenderer (..)
  , selectRenderer
  , render
  , reportDiagnostic
  ) where

import qualified Data.Aeson                     as Aeson
import qualified Data.ByteString.Lazy.Char8     as BSL8
import           Data.Maybe                     (fromMaybe)
import           Data.Text                      (Text)
import qualified Data.Text.IO                   as TIO
import           System.IO                       (stdout)
import           Prettyprinter                  (Doc, defaultLayoutOptions, layoutPretty)
import           Prettyprinter.Render.Text      (renderStrict)

import           Tadka.Internal.Ann             (Ann)
import           Tadka.Internal.Config
                   (Config, Target (..), configColorMode, configPalette,
                    configContextLines, configHyperlinkMode, configRelatedDepth,
                    configTabWidth, configTarget, configUnicodeMode)
import           Tadka.Internal.Diagnostic      (Diagnostic)
import           Tadka.Internal.Terminal        (detectTerminalCaps, resolveConfig)
import           Tadka.Internal.Renderer.Graphical
                   (GraphicalOptions (..), renderGraphical)
import           Tadka.Internal.Renderer.Json
                   (JsonOptions (..), renderJson)
import           Tadka.Internal.Renderer.Narratable
                   (NarratableOptions (..), renderNarratable)

-- | The output type each target renders to. Closed family.
type family Output (t :: Target) where
  Output 'TGraphical  = Doc Ann
  Output 'TNarratable = Text
  Output 'TJson       = Aeson.Value

-- | A renderer indexed by its 'Target'. The three constructors are exported so
-- call sites can pattern-match, but an @*Options@ value can only come from
-- 'selectRenderer', so this is the only route to a renderer that does anything.
data Renderer (t :: Target) where
  Graphical  :: GraphicalOptions  -> Renderer 'TGraphical
  Narratable :: NarratableOptions -> Renderer 'TNarratable
  Json       :: JsonOptions       -> Renderer 'TJson

-- | A renderer with its target hidden, as returned by 'selectRenderer'.
data SomeRenderer = forall t. SomeRenderer (Renderer t)


selectRenderer :: Config -> SomeRenderer
selectRenderer cfg =
  case fromMaybe TGraphical (configTarget cfg) of
    TGraphical ->
      SomeRenderer . Graphical $
        GraphicalOptions
          { goColorMode     = configColorMode cfg
          , goUnicodeMode   = configUnicodeMode cfg
          , goHyperlinkMode = configHyperlinkMode cfg
          , goPalette       = configPalette cfg
          , goRelatedDepth  = configRelatedDepth cfg
          , goTabWidth      = configTabWidth cfg
          , goContextLines  = configContextLines cfg
          }
    TNarratable ->
      SomeRenderer (Narratable (NarratableOptions (configRelatedDepth cfg)))
    TJson ->
      SomeRenderer (Json (JsonOptions (configRelatedDepth cfg)))

-- | Render a diagnostic with a specific renderer. Signature fixed here; each
-- arm dispatches to its handler body.
render :: Diagnostic e => Renderer t -> e -> Output t
render (Graphical opts)  e = renderGraphical opts e
render (Narratable opts) e = renderNarratable opts e
render (Json opts)       e = renderJson opts e


reportDiagnostic :: Diagnostic e => Config -> e -> IO ()
reportDiagnostic cfg e = do
  caps <- detectTerminalCaps stdout
  case selectRenderer (resolveConfig caps cfg) of
    SomeRenderer r@(Graphical _) ->
      TIO.putStrLn (renderStrict (layoutPretty defaultLayoutOptions (render r e)))
    SomeRenderer r@(Narratable _) ->
      TIO.putStrLn (render r e)
    SomeRenderer r@(Json _) ->
      BSL8.putStrLn (Aeson.encode (render r e))