packages feed

patat-0.2.0.0: src/Patat/Theme.hs

--------------------------------------------------------------------------------
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE TemplateHaskell            #-}
module Patat.Theme
    ( Theme (..)
    , defaultTheme
    , Style (..)
    ) where


--------------------------------------------------------------------------------
import           Control.Monad          (mplus)
import qualified Data.Aeson             as A
import qualified Data.Aeson.TH.Extended as A
import           Data.Char              (toUpper)
import           Data.List              (intercalate)
import qualified Data.Map               as M
import           Data.Maybe             (mapMaybe, maybeToList)
import           Data.Monoid            (Monoid (..), (<>))
import qualified Data.Text              as T
import qualified System.Console.ANSI    as Ansi
import           Prelude


--------------------------------------------------------------------------------
data Theme = Theme
    { themeBorders           :: !(Maybe Style)
    , themeHeader            :: !(Maybe Style)
    , themeCodeBlock         :: !(Maybe Style)
    , themeBulletList        :: !(Maybe Style)
    , themeBulletListMarkers :: !(Maybe T.Text)
    , themeOrderedList       :: !(Maybe Style)
    , themeBlockQuote        :: !(Maybe Style)
    , themeDefinitionTerm    :: !(Maybe Style)
    , themeDefinitionList    :: !(Maybe Style)
    , themeTableHeader       :: !(Maybe Style)
    , themeTableSeparator    :: !(Maybe Style)
    , themeEmph              :: !(Maybe Style)
    , themeStrong            :: !(Maybe Style)
    , themeCode              :: !(Maybe Style)
    , themeLinkText          :: !(Maybe Style)
    , themeLinkTarget        :: !(Maybe Style)
    , themeStrikeout         :: !(Maybe Style)
    , themeQuoted            :: !(Maybe Style)
    , themeMath              :: !(Maybe Style)
    , themeImageText         :: !(Maybe Style)
    , themeImageTarget       :: !(Maybe Style)
    } deriving (Show)


--------------------------------------------------------------------------------
instance Monoid Theme where
    mempty = Theme
        Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
        Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
        Nothing Nothing Nothing

    mappend l r = Theme
        { themeBorders           = mplusOn themeBorders
        , themeHeader            = mplusOn themeHeader
        , themeCodeBlock         = mplusOn themeCodeBlock
        , themeBulletList        = mplusOn themeBulletList
        , themeBulletListMarkers = mplusOn themeBulletListMarkers
        , themeOrderedList       = mplusOn themeOrderedList
        , themeBlockQuote        = mplusOn themeBlockQuote
        , themeDefinitionTerm    = mplusOn themeDefinitionTerm
        , themeDefinitionList    = mplusOn themeDefinitionList
        , themeTableHeader       = mplusOn themeTableHeader
        , themeTableSeparator    = mplusOn themeTableSeparator
        , themeEmph              = mplusOn themeEmph
        , themeStrong            = mplusOn themeStrong
        , themeCode              = mplusOn themeCode
        , themeLinkText          = mplusOn themeLinkText
        , themeLinkTarget        = mplusOn themeLinkTarget
        , themeStrikeout         = mplusOn themeStrikeout
        , themeQuoted            = mplusOn themeQuoted
        , themeMath              = mplusOn themeMath
        , themeImageText         = mplusOn themeImageText
        , themeImageTarget       = mplusOn themeImageTarget
        }
      where
        mplusOn f = f l `mplus` f r


--------------------------------------------------------------------------------
defaultTheme :: Theme
defaultTheme = Theme
    { themeBorders           = dull Ansi.Yellow
    , themeHeader            = dull Ansi.Blue
    , themeCodeBlock         = dull Ansi.White <> ondull Ansi.Black
    , themeBulletList        = dull Ansi.Magenta
    , themeBulletListMarkers = Just "-*"
    , themeOrderedList       = dull Ansi.Magenta
    , themeBlockQuote        = dull Ansi.Green
    , themeDefinitionTerm    = dull Ansi.Blue
    , themeDefinitionList    = dull Ansi.Magenta
    , themeTableHeader       = dull Ansi.Blue
    , themeTableSeparator    = dull Ansi.Magenta
    , themeEmph              = dull Ansi.Green
    , themeStrong            = dull Ansi.Red <> bold
    , themeCode              = dull Ansi.White <> ondull Ansi.Black
    , themeLinkText          = dull Ansi.Green
    , themeLinkTarget        = dull Ansi.Cyan <> underline
    , themeStrikeout         = ondull Ansi.Red
    , themeQuoted            = dull Ansi.Green
    , themeMath              = dull Ansi.Green
    , themeImageText         = dull Ansi.Green
    , themeImageTarget       = dull Ansi.Cyan <> underline
    }
  where
    dull   c  = Just $ Style [Ansi.SetColor Ansi.Foreground Ansi.Dull c]
    ondull c  = Just $ Style [Ansi.SetColor Ansi.Background Ansi.Dull c]
    bold      = Just $ Style [Ansi.SetConsoleIntensity Ansi.BoldIntensity]
    underline = Just $ Style [Ansi.SetUnderlining Ansi.SingleUnderline]


--------------------------------------------------------------------------------
newtype Style = Style {unStyle :: [Ansi.SGR]}
    deriving (Monoid, Show)


--------------------------------------------------------------------------------
instance A.ToJSON Style where
    toJSON = A.toJSON . mapMaybe nameForSGR . unStyle


--------------------------------------------------------------------------------
instance A.FromJSON Style where
    parseJSON val = do
        names <- A.parseJSON val
        sgrs  <- mapM toSgr names
        return $! Style sgrs
      where
        toSgr name = case M.lookup name sgrsByName of
            Just sgr -> return sgr
            Nothing  -> fail $!
                "Unknown style: " ++ show name ++ ". Known styles are: " ++
                intercalate ", " (map show $ M.keys sgrsByName)


--------------------------------------------------------------------------------
nameForSGR :: Ansi.SGR -> Maybe String
nameForSGR (Ansi.SetColor layer intensity color) = Just $
    (\str -> case layer of
        Ansi.Foreground -> str
        Ansi.Background -> "on" ++ capitalize str) $
    (case intensity of
        Ansi.Dull  -> "dull"
        Ansi.Vivid -> "vivid") ++
    (case color of
        Ansi.Black   -> "Black"
        Ansi.Red     -> "Red"
        Ansi.Green   -> "Green"
        Ansi.Yellow  -> "Yellow"
        Ansi.Blue    -> "Blue"
        Ansi.Magenta -> "Magenta"
        Ansi.Cyan    -> "Cyan"
        Ansi.White   -> "White")
  where
    capitalize ""       = ""
    capitalize (x : xs) = toUpper x : xs

nameForSGR (Ansi.SetUnderlining Ansi.SingleUnderline) = Just "underline"

nameForSGR (Ansi.SetConsoleIntensity Ansi.BoldIntensity) = Just "bold"

nameForSGR _ = Nothing


--------------------------------------------------------------------------------
sgrsByName :: M.Map String Ansi.SGR
sgrsByName = M.fromList
    [ (name, sgr)
    | sgr  <- knownSgrs
    , name <- maybeToList (nameForSGR sgr)
    ]
  where
    -- | It doesn't really matter if we generate "too much" SGRs here since
    -- 'nameForSGR' will only pick the ones we support.
    knownSgrs =
        [ Ansi.SetColor l i c
        | l <- [minBound .. maxBound]
        , i <- [minBound .. maxBound]
        , c <- [minBound .. maxBound]
        ] ++
        [Ansi.SetUnderlining      u | u <- [minBound .. maxBound]] ++
        [Ansi.SetConsoleIntensity c | c <- [minBound .. maxBound]]


--------------------------------------------------------------------------------
$(A.deriveJSON A.dropPrefixOptions ''Theme)