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