1{-# LANGUAGE CPP #-} 2{-# LANGUAGE DeriveDataTypeable #-} 3{-# LANGUAGE DeriveGeneric #-} 4{-# LANGUAGE FlexibleContexts #-} 5{-# LANGUAGE LambdaCase #-} 6{-# LANGUAGE OverloadedStrings #-} 7{-# LANGUAGE RankNTypes #-} 8{-# LANGUAGE RecordWildCards #-} 9{-# LANGUAGE ScopedTypeVariables #-} 10 11{-| 12Module : Text.Pretty.Simple.Internal.Printer 13Copyright : (c) Dennis Gosnell, 2016 14License : BSD-style (see LICENSE file) 15Maintainer : cdep.illabout@gmail.com 16Stability : experimental 17Portability : POSIX 18 19-} 20module Text.Pretty.Simple.Internal.Printer 21 where 22 23-- We don't need these imports for later GHCs as all required functions 24-- are exported from Prelude 25#if __GLASGOW_HASKELL__ < 710 26import Control.Applicative 27#endif 28#if __GLASGOW_HASKELL__ < 804 29import Data.Monoid ((<>)) 30#endif 31 32import Control.Monad.IO.Class (MonadIO, liftIO) 33import Control.Monad (join) 34import Control.Monad.State (MonadState, evalState, modify, gets) 35import Data.Char (isPrint, isSpace, ord) 36import Data.List (dropWhileEnd) 37import Data.List.NonEmpty (NonEmpty, nonEmpty) 38import Data.Maybe (fromMaybe) 39import Prettyprinter 40 (indent, line', PageWidth(AvailablePerLine), layoutPageWidth, nest, hsep, 41 concatWith, space, Doc, SimpleDocStream, annotate, defaultLayoutOptions, 42 enclose, hcat, layoutSmart, line, unAnnotateS, pretty, group) 43import Data.Typeable (Typeable) 44import GHC.Generics (Generic) 45import Numeric (showHex) 46import System.IO (Handle, hIsTerminalDevice) 47import Text.Read (readMaybe) 48 49import Text.Pretty.Simple.Internal.Expr 50 (Expr(..), CommaSeparated(CommaSeparated)) 51import Text.Pretty.Simple.Internal.ExprParser (expressionParse) 52import Text.Pretty.Simple.Internal.Color 53 (colorNull, Style, ColorOptions(..), defaultColorOptionsDarkBg, 54 defaultColorOptionsLightBg) 55 56-- $setup 57-- >>> import Text.Pretty.Simple (pPrintString, pPrintStringOpt) 58 59-- | Determines whether pretty-simple should check if the output 'Handle' is a 60-- TTY device. Normally, users only want to print in color if the output 61-- 'Handle' is a TTY device. 62data CheckColorTty 63 = CheckColorTty 64 -- ^ Check if the output 'Handle' is a TTY device. If the output 'Handle' is 65 -- a TTY device, determine whether to print in color based on 66 -- 'outputOptionsColorOptions'. If not, then set 'outputOptionsColorOptions' 67 -- to 'Nothing' so the output does not get colorized. 68 | NoCheckColorTty 69 -- ^ Don't check if the output 'Handle' is a TTY device. Determine whether to 70 -- colorize the output based solely on the value of 71 -- 'outputOptionsColorOptions'. 72 deriving (Eq, Generic, Show, Typeable) 73 74-- | Control how escaped and non-printable are output for strings. 75-- 76-- See 'outputOptionsStringStyle' for what the output looks like with each of 77-- these options. 78data StringOutputStyle 79 = Literal 80 -- ^ Output string literals by printing the source characters exactly. 81 -- 82 -- For examples: without this option the printer will insert a newline in 83 -- place of @"\n"@, with this options the printer will output @'\'@ and 84 -- @'n'@. Similarly the exact escape codes used in the input string will be 85 -- replicated, so @"\65"@ will be printed as @"\65"@ and not @"A"@. 86 | EscapeNonPrintable 87 -- ^ Replace non-printable characters with hexadecimal escape sequences. 88 | DoNotEscapeNonPrintable 89 -- ^ Output non-printable characters without modification. 90 deriving (Eq, Generic, Show, Typeable) 91 92-- | Data-type wrapping up all the options available when rendering the list 93-- of 'Output's. 94data OutputOptions = OutputOptions 95 { outputOptionsIndentAmount :: Int 96 -- ^ Number of spaces to use when indenting. It should probably be either 2 97 -- or 4. 98 , outputOptionsPageWidth :: Int 99 -- ^ The maximum number of characters to fit on to one line. 100 , outputOptionsCompact :: Bool 101 -- ^ Use less vertical (and more horizontal) space. 102 , outputOptionsCompactParens :: Bool 103 -- ^ Group closing parentheses on to a single line. 104 , outputOptionsInitialIndent :: Int 105 -- ^ Indent the whole output by this amount. 106 , outputOptionsColorOptions :: Maybe ColorOptions 107 -- ^ If this is 'Nothing', then don't colorize the output. If this is 108 -- @'Just' colorOptions@, then use @colorOptions@ to colorize the output. 109 -- 110 , outputOptionsStringStyle :: StringOutputStyle 111 -- ^ Controls how string literals are output. 112 -- 113 -- By default, the pPrint functions escape non-printable characters, but 114 -- print all printable characters: 115 -- 116 -- >>> pPrintString "\"A \\x42 Ä \\xC4 \\x1 \\n\"" 117 -- "A B Ä Ä \x1 118 -- " 119 -- 120 -- Here, you can see that the character @A@ has been printed as-is. @\x42@ 121 -- has been printed in the non-escaped version, @B@. The non-printable 122 -- character @\x1@ has been printed as @\x1@. Newlines will be removed to 123 -- make the output easier to read. 124 -- 125 -- This corresponds to the 'StringOutputStyle' called 'EscapeNonPrintable'. 126 -- 127 -- (Note that in the above and following examples, the characters have to be 128 -- double-escaped, which makes it somewhat confusing...) 129 -- 130 -- Another output style is 'DoNotEscapeNonPrintable'. This is similar 131 -- to 'EscapeNonPrintable', except that non-printable characters get printed 132 -- out literally to the screen. 133 -- 134 -- >>> pPrintStringOpt CheckColorTty defaultOutputOptionsDarkBg{ outputOptionsStringStyle = DoNotEscapeNonPrintable } "\"A \\x42 Ä \\xC4 \\n\"" 135 -- "A B Ä Ä 136 -- " 137 -- 138 -- If you change the above example to contain @\x1@, you can see that it is 139 -- output as a literal, non-escaped character. Newlines are still removed 140 -- for readability. 141 -- 142 -- Another output style is 'Literal'. This just outputs all escape characters. 143 -- 144 -- >>> pPrintStringOpt CheckColorTty defaultOutputOptionsDarkBg{ outputOptionsStringStyle = Literal } "\"A \\x42 Ä \\xC4 \\x1 \\n\"" 145 -- "A \x42 Ä \xC4 \x1 \n" 146 -- 147 -- You can see that all the escape characters get output literally, including 148 -- newline. 149 } deriving (Eq, Generic, Show, Typeable) 150 151-- | Default values for 'OutputOptions' when printing to a console with a dark 152-- background. 'outputOptionsIndentAmount' is 4, and 153-- 'outputOptionsColorOptions' is 'defaultColorOptionsDarkBg'. 154defaultOutputOptionsDarkBg :: OutputOptions 155defaultOutputOptionsDarkBg = 156 defaultOutputOptionsNoColor 157 { outputOptionsColorOptions = Just defaultColorOptionsDarkBg } 158 159-- | Default values for 'OutputOptions' when printing to a console with a light 160-- background. 'outputOptionsIndentAmount' is 4, and 161-- 'outputOptionsColorOptions' is 'defaultColorOptionsLightBg'. 162defaultOutputOptionsLightBg :: OutputOptions 163defaultOutputOptionsLightBg = 164 defaultOutputOptionsNoColor 165 { outputOptionsColorOptions = Just defaultColorOptionsLightBg } 166 167-- | Default values for 'OutputOptions' when printing using using ANSI escape 168-- sequences for color. 'outputOptionsIndentAmount' is 4, and 169-- 'outputOptionsColorOptions' is 'Nothing'. 170defaultOutputOptionsNoColor :: OutputOptions 171defaultOutputOptionsNoColor = 172 OutputOptions 173 { outputOptionsIndentAmount = 4 174 , outputOptionsPageWidth = 80 175 , outputOptionsCompact = False 176 , outputOptionsCompactParens = False 177 , outputOptionsInitialIndent = 0 178 , outputOptionsColorOptions = Nothing 179 , outputOptionsStringStyle = EscapeNonPrintable 180 } 181 182-- | Given 'OutputOptions', disable colorful output if the given handle 183-- is not connected to a TTY. 184hCheckTTY :: MonadIO m => Handle -> OutputOptions -> m OutputOptions 185hCheckTTY h options = liftIO $ conv <$> tty 186 where 187 conv :: Bool -> OutputOptions 188 conv True = options 189 conv False = options { outputOptionsColorOptions = Nothing } 190 191 tty :: IO Bool 192 tty = hIsTerminalDevice h 193 194-- | Parse a string, and generate an intermediate representation, 195-- suitable for passing to any /prettyprinter/ backend. 196-- Used by 'Simple.pString' etc. 197layoutString :: OutputOptions -> String -> SimpleDocStream Style 198layoutString opts = 199 annotateStyle opts 200 . layoutSmart defaultLayoutOptions 201 {layoutPageWidth = AvailablePerLine (outputOptionsPageWidth opts) 1} 202 . indent (outputOptionsInitialIndent opts) 203 . prettyExprs' opts 204 . preprocess opts 205 . expressionParse 206 207-- | Slight adjustment of 'prettyExprs' for the outermost level, 208-- to avoid indenting everything. 209prettyExprs' :: OutputOptions -> [Expr] -> Doc Annotation 210prettyExprs' opts = \case 211 [] -> mempty 212 x : xs -> prettyExpr opts x <> prettyExprs opts xs 213 214-- | Construct a 'Doc' from multiple 'Expr's. 215prettyExprs :: OutputOptions -> [Expr] -> Doc Annotation 216prettyExprs opts = hcat . map subExpr 217 where 218 subExpr x = 219 let doc = prettyExpr opts x 220 in 221 if isSimple x then 222 -- keep the expression on the current line 223 nest 2 $ space <> doc 224 else 225 -- put the expression on a new line, indented (unless grouped) 226 nest (outputOptionsIndentAmount opts) $ line <> doc 227 228-- | Construct a 'Doc' from a single 'Expr'. 229prettyExpr :: OutputOptions -> Expr -> Doc Annotation 230prettyExpr opts = (if outputOptionsCompact opts then group else id) . \case 231 Brackets xss -> list "[" "]" xss 232 Braces xss -> list "{" "}" xss 233 Parens xss -> list "(" ")" xss 234 StringLit s -> join enclose (annotate Quote "\"") $ annotate String $ pretty s 235 CharLit s -> join enclose (annotate Quote "'") $ annotate String $ pretty s 236 Other s -> pretty s 237 NumberLit n -> annotate Num $ pretty n 238 where 239 list :: Doc Annotation -> Doc Annotation -> CommaSeparated [Expr] 240 -> Doc Annotation 241 list open close (CommaSeparated xss) = 242 enclose (annotate Open open) (annotate Close close) $ case xss of 243 [] -> mempty 244 [xs] | all isSimple xs -> 245 space <> hsep (map (prettyExpr opts) xs) <> space 246 _ -> concatWith lineAndCommaSep (map (prettyExprs opts) xss) 247 <> if outputOptionsCompactParens opts then space else line 248 lineAndCommaSep x y = x <> line' <> annotate Comma "," <> y 249 250-- | Determine whether this expression should be displayed on a single line. 251isSimple :: Expr -> Bool 252isSimple = \case 253 Brackets (CommaSeparated xs) -> isListSimple xs 254 Braces (CommaSeparated xs) -> isListSimple xs 255 Parens (CommaSeparated xs) -> isListSimple xs 256 _ -> True 257 where 258 isListSimple = \case 259 [[e]] -> isSimple e 260 _:_ -> False 261 [] -> True 262 263-- | Traverse the stream, using a 'Tape' to keep track of the current style. 264annotateStyle :: OutputOptions -> SimpleDocStream Annotation 265 -> SimpleDocStream Style 266annotateStyle opts ds = case outputOptionsColorOptions opts of 267 Nothing -> unAnnotateS ds 268 Just ColorOptions {..} -> evalState (traverse style ds) initialTape 269 where 270 style :: MonadState (Tape Style) m => Annotation -> m Style 271 style = \case 272 Open -> modify moveR *> gets tapeHead 273 Close -> gets tapeHead <* modify moveL 274 Comma -> gets tapeHead 275 Quote -> pure colorQuote 276 String -> pure colorString 277 Num -> pure colorNum 278 initialTape = Tape 279 { tapeLeft = streamRepeat colorError 280 , tapeHead = colorError 281 , tapeRight = streamCycle $ fromMaybe (pure colorNull) 282 $ nonEmpty colorRainbowParens 283 } 284 285-- | An abstract annotation type, representing the various elements 286-- we may want to highlight. 287data Annotation 288 = Open 289 | Close 290 | Comma 291 | Quote 292 | String 293 | Num 294 295-- | Apply various transformations to clean up the 'Expr's. 296preprocess :: OutputOptions -> [Expr] -> [Expr] 297preprocess opts = map processExpr . removeEmptyOthers 298 where 299 processExpr = \case 300 Brackets xss -> Brackets $ cs xss 301 Braces xss -> Braces $ cs xss 302 Parens xss -> Parens $ cs xss 303 StringLit s -> StringLit $ 304 case outputOptionsStringStyle opts of 305 Literal -> s 306 EscapeNonPrintable -> escapeNonPrintable $ readStr s 307 DoNotEscapeNonPrintable -> readStr s 308 CharLit s -> CharLit s 309 Other s -> Other $ shrinkWhitespace $ strip s 310 NumberLit n -> NumberLit n 311 cs (CommaSeparated ess) = CommaSeparated $ map (preprocess opts) ess 312 readStr :: String -> String 313 readStr s = fromMaybe s . readMaybe $ '"': s ++ "\"" 314 315-- | Remove any 'Other' 'Expr's which contain only spaces. 316-- These provide no value, but mess up formatting if left in. 317removeEmptyOthers :: [Expr] -> [Expr] 318removeEmptyOthers = filter $ \case 319 Other s -> not $ all isSpace s 320 _ -> True 321 322-- | Replace non-printable characters with hex escape sequences. 323-- 324-- >>> escapeNonPrintable "\x1\x2" 325-- "\\x1\\x2" 326-- 327-- Newlines will not be escaped. 328-- 329-- >>> escapeNonPrintable "hello\nworld" 330-- "hello\nworld" 331-- 332-- Printable characters will not be escaped. 333-- 334-- >>> escapeNonPrintable "h\101llo" 335-- "hello" 336escapeNonPrintable :: String -> String 337escapeNonPrintable input = foldr escape "" input 338 339-- | Replace an unprintable character except a newline 340-- with a hex escape sequence. 341escape :: Char -> ShowS 342escape c 343 | isPrint c || c == '\n' = (c:) 344 | otherwise = ('\\':) . ('x':) . showHex (ord c) 345 346-- | Compress multiple whitespaces to just one whitespace. 347-- 348-- >>> shrinkWhitespace " hello there " 349-- " hello there " 350shrinkWhitespace :: String -> String 351shrinkWhitespace (' ':' ':t) = shrinkWhitespace (' ':t) 352shrinkWhitespace (h:t) = h : shrinkWhitespace t 353shrinkWhitespace "" = "" 354 355-- | Remove trailing and leading whitespace (see 'Data.Text.strip'). 356-- 357-- >>> strip " hello there " 358-- "hello there" 359strip :: String -> String 360strip = dropWhile isSpace . dropWhileEnd isSpace 361 362-- | A bidirectional Turing-machine tape: 363-- infinite in both directions, with a head pointing to one element. 364data Tape a = Tape 365 { tapeLeft :: Stream a -- ^ the side of the 'Tape' left of 'tapeHead' 366 , tapeHead :: a -- ^ the focused element 367 , tapeRight :: Stream a -- ^ the side of the 'Tape' right of 'tapeHead' 368 } deriving Show 369-- | Move the head left 370moveL :: Tape a -> Tape a 371moveL (Tape (l :.. ls) c rs) = Tape ls l (c :.. rs) 372-- | Move the head right 373moveR :: Tape a -> Tape a 374moveR (Tape ls c (r :.. rs)) = Tape (c :.. ls) r rs 375 376-- | An infinite list 377data Stream a = a :.. Stream a deriving Show 378-- | Analogous to 'repeat' 379streamRepeat :: t -> Stream t 380streamRepeat x = x :.. streamRepeat x 381-- | Analogous to 'cycle' 382-- While the inferred signature here is more general, 383-- it would diverge on an empty structure 384streamCycle :: NonEmpty a -> Stream a 385streamCycle xs = foldr (:..) (streamCycle xs) xs 386