{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-| Module : Text.Pretty.Simple.Internal.OutputPrinter Copyright : (c) Dennis Gosnell, 2016 License : BSD-style (see LICENSE file) Maintainer : cdep.illabout@gmail.com Stability : experimental Portability : POSIX -} module Text.Pretty.Simple.Internal.OutputPrinter where #if __GLASGOW_HASKELL__ < 710 -- We don't need this import for GHC 7.10 as it exports all required functions -- from Prelude import Control.Applicative #endif import Control.Monad.Reader (MonadReader(reader), runReader) import Data.Data (Data) import Data.Foldable (fold, foldlM) import Data.Text.Lazy (Text) import Data.Text.Lazy.Builder (Builder, fromString, toLazyText) import Data.Typeable (Typeable) import GHC.Generics (Generic) import System.Console.ANSI (Color(..), ColorIntensity(..), ConsoleIntensity(..), ConsoleLayer(..), SGR(..), setSGRCode) import Text.Pretty.Simple.Internal.Output (NestLevel(..), Output(..), OutputType(..)) -- | 'UseColor' describes whether or not we want to use color when printing the -- 'Output' list. data UseColor = NoColor | UseColor deriving (Data, Eq, Generic, Read, Show, Typeable) -- | Data-type wrapping up all the options available when rendering the list -- of 'Output's. data OutputOptions = OutputOptions { outputOptionsIndentAmount :: Int -- ^ Number of spaces to use when indenting. It should probably be either 2 -- or 4. , outputOptionsUseColor :: UseColor -- ^ Whether or not to use ansi escape sequences to print colors. } deriving (Data, Eq, Generic, Read, Show, Typeable) -- | Default values for 'OutputOptions'. 'outputOptionsIndentAmount' defaults -- to 4, and 'outputOptionsUseColor' defaults to 'UseColor'. defaultOutputOptions :: OutputOptions defaultOutputOptions = OutputOptions {outputOptionsIndentAmount = 4, outputOptionsUseColor = UseColor} render :: OutputOptions -> [Output] -> Text render options outputs = toLazyText $ runReader (renderOutputs outputs) options renderOutputs :: forall m. MonadReader OutputOptions m => [Output] -> m Builder renderOutputs = foldlM foldFunc "" . modificationsOutputList where foldFunc :: Builder -> Output -> m Builder foldFunc accum output = mappend accum <$> renderOutput output renderRaibowParenFor :: MonadReader OutputOptions m => NestLevel -> Builder -> m Builder renderRaibowParenFor nest string = sequenceFold [rainbowParen nest, pure string, colorReset] renderOutput :: MonadReader OutputOptions m => Output -> m Builder renderOutput (Output nest OutputCloseBrace) = renderRaibowParenFor nest "}" renderOutput (Output nest OutputCloseBracket) = renderRaibowParenFor nest "]" renderOutput (Output nest OutputCloseParen) = renderRaibowParenFor nest ")" renderOutput (Output nest OutputComma) = renderRaibowParenFor nest "," renderOutput (Output _ OutputIndent) = do indentSpaces <- reader outputOptionsIndentAmount pure . mconcat $ replicate indentSpaces " " renderOutput (Output _ OutputNewLine) = pure "\n" renderOutput (Output nest OutputOpenBrace) = renderRaibowParenFor nest "{" renderOutput (Output nest OutputOpenBracket) = renderRaibowParenFor nest "[" renderOutput (Output nest OutputOpenParen) = renderRaibowParenFor nest "(" renderOutput (Output _ (OutputOther string)) = -- TODO: This probably shouldn't be a string to begin with. pure $ fromString string renderOutput (Output _ (OutputStringLit string)) = do sequenceFold [ colorQuote , pure "\"" , colorString -- TODO: This probably shouldn't be a string to begin with. , pure $ fromString string , colorQuote , pure "\"" , colorReset ] sequenceFold :: (Monad f, Monoid a, Traversable t) => t (f a) -> f a sequenceFold = fmap fold . sequence -- | A function that performs optimizations and modifications to a list of -- input 'Output's. -- -- An sample of an optimization is 'removeStartingNewLine' which just removes a -- newline if it is the first item in an 'Output' list. modificationsOutputList :: [Output] -> [Output] modificationsOutputList = shrinkWhitespaceInOthers . compressOthers . removeStartingNewLine -- | Remove a 'OutputNewLine' if it is the first item in the 'Output' list. -- -- >>> removeStartingNewLine [Output 3 OutputNewLine, Output 3 OutputComma] -- [Output {outputNestLevel = NestLevel {unNestLevel = 3}, outputOutputType = OutputComma}] removeStartingNewLine :: [Output] -> [Output] removeStartingNewLine ((Output _ OutputNewLine) : t) = t removeStartingNewLine outputs = outputs -- | If there are two subsequent 'OutputOther' tokens, combine them into just -- one 'OutputOther'. -- -- >>> compressOthers [Output 0 (OutputOther "foo"), Output 0 (OutputOther "bar")] -- [Output {outputNestLevel = NestLevel {unNestLevel = 0}, outputOutputType = OutputOther "foobar"}] compressOthers :: [Output] -> [Output] compressOthers [] = [] compressOthers (Output _ (OutputOther string1):(Output nest (OutputOther string2)):t) = compressOthers ((Output nest (OutputOther (string1 `mappend` string2))) : t) compressOthers (h:t) = h : compressOthers t -- | In each 'OutputOther' token, compress multiple whitespaces to just one -- whitespace. -- -- >>> shrinkWhitespaceInOthers [Output 0 (OutputOther " hello ")] -- [Output {outputNestLevel = NestLevel {unNestLevel = 0}, outputOutputType = OutputOther " hello "}] shrinkWhitespaceInOthers :: [Output] -> [Output] shrinkWhitespaceInOthers = fmap shrinkWhitespaceInOther shrinkWhitespaceInOther :: Output -> Output shrinkWhitespaceInOther (Output nest (OutputOther string)) = Output nest . OutputOther $ shrinkWhitespace string shrinkWhitespaceInOther other = other shrinkWhitespace :: String -> String shrinkWhitespace (' ':' ':t) = shrinkWhitespace (' ':t) shrinkWhitespace (h:t) = h : shrinkWhitespace t shrinkWhitespace "" = "" ----------------------- -- High-level colors -- ----------------------- colorQuote :: MonadReader OutputOptions m => m Builder colorQuote = appendColors colorBold colorVividWhite colorString :: MonadReader OutputOptions m => m Builder colorString = appendColors colorBold colorVividBlue colorError :: MonadReader OutputOptions m => m Builder colorError = appendColors colorBold colorVividRed colorNum :: MonadReader OutputOptions m => m Builder colorNum = appendColors colorBold colorVividGreen rainbowParen :: forall m. MonadReader OutputOptions m => NestLevel -> m Builder rainbowParen (NestLevel nestLevel) = let choicesLen = length rainbowParenChoices in rainbowParenChoices !! (nestLevel `mod` choicesLen) where rainbowParenChoices :: [m Builder] rainbowParenChoices = [ appendColors colorBold colorVividMagenta , appendColors colorBold colorVividCyan , appendColors colorBold colorVividYellow ] ---------------------- -- Low-level Colors -- ---------------------- canUseColor :: MonadReader OutputOptions m => m Bool canUseColor = do color <- reader outputOptionsUseColor case color of NoColor -> pure False UseColor -> pure True ifM :: Monad m => m Bool -> a -> a -> m a ifM comparisonM thenValue elseValue = do res <- comparisonM case res of True -> pure thenValue False -> pure elseValue colorBold :: MonadReader OutputOptions m => m Builder colorBold = ifM canUseColor (setSGRCodeBuilder [SetConsoleIntensity BoldIntensity]) "" colorReset :: MonadReader OutputOptions m => m Builder colorReset = ifM canUseColor (setSGRCodeBuilder [Reset]) "" colorVividBlue :: MonadReader OutputOptions m => m Builder colorVividBlue = colorHelper Vivid Blue colorVividCyan :: MonadReader OutputOptions m => m Builder colorVividCyan = colorHelper Vivid Cyan colorVividGreen :: MonadReader OutputOptions m => m Builder colorVividGreen = colorHelper Vivid Green colorVividMagenta :: MonadReader OutputOptions m => m Builder colorVividMagenta = colorHelper Vivid Magenta colorVividRed :: MonadReader OutputOptions m => m Builder colorVividRed = colorHelper Vivid Red colorVividWhite :: MonadReader OutputOptions m => m Builder colorVividWhite = colorHelper Vivid White colorVividYellow :: MonadReader OutputOptions m => m Builder colorVividYellow = colorHelper Vivid Yellow colorHelper :: MonadReader OutputOptions m => ColorIntensity -> Color -> m Builder colorHelper colorIntensity color = ifM canUseColor (setSGRCodeBuilder [SetColor Foreground colorIntensity color]) "" appendColors :: MonadReader OutputOptions m => m Builder -> m Builder -> m Builder appendColors color1 color2 = mappend <$> color1 <*> color2 setSGRCodeBuilder :: [SGR] -> Builder setSGRCodeBuilder = fromString . setSGRCode