show-prettyprint-0.3: src/Text/Show/Prettyprint/Internal.hs
{-# LANGUAGE LambdaCase #-}
-- | __This module may change arbitrarily between versions.__ It is exposed only
-- for documentary purposes.
module Text.Show.Prettyprint.Internal (
parseShowString,
shownP,
valueP,
identifierP,
numberP,
stringLitP,
charLitP,
argP,
unitP,
tupleP,
listP,
recordP,
) where
import Control.Applicative
import Data.Text.Prettyprint.Doc as Ppr
import Text.Trifecta as Tri
-- $setup
--
-- >>> import Text.PrettyPrint.ANSI.Leijen (plain)
-- >>> :{
-- let testParse p s = case parseString p mempty s of
-- Success x -> print x
-- Failure ErrInfo{ _errDoc = e } -> putStrLn ("ERROR " ++ show (plain e))
-- :}
parseShowString :: String -> Result (Doc ann)
parseShowString = parseString shownP mempty
-- | Prettyparser for a 'show'-generated string
shownP :: Parser (Doc ann)
shownP = valueP <* eof
-- | Prettyparser for a constructor, which is roughly a word applied to
-- arguments.
--
-- >>> testParse valueP "Just ('c', Left ())"
-- Just ('c',Left ())
valueP :: Parser (Doc ann)
valueP = do
thing <- choice [identifierP, numberP, stringLitP, charLitP, listP]
args <- many argP
pure (if null args
then thing
else thing <+> align (sep args) )
-- | An identifier is a liberal version of a "variable or constructor", which
-- roughly means that it's a printable word without parentheses.
--
-- >>> testParse identifierP "_foo'bar"
-- _foo'bar
identifierP :: Parser (Doc ann)
identifierP = token (p <?> "identifier")
where
p = fmap Ppr.pretty (some (alphaNum <|> oneOf "'_"))
-- | Number in integer or scientific notation.
--
-- >>> testParse numberP "123456"
-- 123456
--
-- >>> testParse numberP "-123.4e56"
-- -1.234e58
numberP :: Parser (Doc ann)
numberP = p <?> "number"
where
p = integerOrDouble >>= \case
Left i -> pure (pretty i)
Right d -> pure (pretty d)
-- |
-- >>> testParse stringLitP "\"Hello\\\\ world!\""
-- "Hello\\ world!"
stringLitP :: Parser (Doc ann)
stringLitP = token (p <?> "string literal")
where
p = fmap (dquotes . pretty . concatMap escapeBackslashes)
(stringLiteral :: Parser String)
escapeBackslashes :: Char -> String
escapeBackslashes '\\' = "\\\\"
escapeBackslashes x = [x]
-- |
-- >>> testParse charLitP "'c'"
-- 'c'
charLitP :: Parser (Doc ann)
charLitP = token (p <?> "char literal")
where
p = fmap (squotes . pretty . escapeBackslashes) Tri.charLiteral
-- $
-- Correct backslash handling
-- >>> testParse charLitP "'\\\\'"
-- '\\'
-- | Anything that could be considered an argument to something else.
--
-- >>> testParse argP "()"
-- ()
--
-- >>> testParse argP "['h', 'e', 'l', 'l', 'o']"
-- ['h','e','l','l','o']
argP :: Parser (Doc ann)
argP = (token . choice) [unitP, tupleP, listP, recordP, valueP]
-- |
-- >>> testParse unitP "()"
-- ()
unitP :: Parser (Doc ann)
unitP = p <?> "unit"
where
p = fmap pretty (Tri.string "()")
-- | Prettyparser for tuples from size 1. Since 1-tuples are just parenthesized
-- expressions to first order approximation, this parser handles those as well.
--
-- >>> testParse tupleP "((), True, 'c')"
-- ((),True,'c')
tupleP :: Parser (Doc ann)
tupleP = p <?> "tuple"
where
p = fmap (encloseSep lparen rparen Ppr.comma) (Tri.parens (do
x <- argP
xs <- many (Tri.comma *> argP)
pure (x:xs) ))
-- | List prettyparser. Lists can be heterogeneous, which is realistic if we
-- consider ill-defined Show instances.
--
-- >>> testParse listP "[\"Hello\", World]"
-- ["Hello",World]
listP :: Parser (Doc ann)
listP = p <?> "list"
where
p = fmap (encloseSep lbracket rbracket Ppr.comma)
(Tri.brackets (sepBy argP Tri.comma))
-- |
-- >>> testParse recordP "{ r1 = (), r2 = Just True }"
-- {r1 = (),r2 = Just True}
recordP :: Parser (Doc ann)
recordP = p <?> "record"
where
p = fmap (encloseSep lbrace rbrace Ppr.comma) (Tri.braces (sepBy recordEntryP Tri.comma))
recordEntryP = do
lhs <- token identifierP
_ <- token (Tri.char '=')
rhs <- argP
pure (lhs <+> pretty '=' <+> align rhs)
-- $
-- Regression (#6): Nested lists were not aligned properly
-- >>> :{
-- testParse valueP (concat
-- ["ApiGroup {name = \"API Key\" ,endpoints = [Endpoint {path = "
-- ,"\"/v1/auth/info\" ,description = \"Retrieve information about the"
-- ," current API key.\" ,needsAPIKey = \"Yes\" ,method = \"GET\" ,"
-- ,"requiredAccess = \"\"} ,Endpoint {path = \"/v1/auth/info\" ,"
-- ,"description = \"Retrieve information about the current API key.\" ,"
-- ,"needsAPIKey = \"Yes\" ,method = \"GET\" ,requiredAccess = \"\"}]}]"
-- ])
-- :}
-- ApiGroup {name = "API Key"
-- ,endpoints = [Endpoint {path = "/v1/auth/info"
-- ,description = "Retrieve information about the current API key."
-- ,needsAPIKey = "Yes"
-- ,method = "GET"
-- ,requiredAccess = ""}
-- ,Endpoint {path = "/v1/auth/info"
-- ,description = "Retrieve information about the current API key."
-- ,needsAPIKey = "Yes"
-- ,method = "GET"
-- ,requiredAccess = ""}]}