th-format-0.1.1.0: src/Data/Format.hs
{-|
Module : Data.Format
Description : QuasiQuoters for simple string interpolation.
Copyright : (c) Moritz Clasmeier, 2017-2018
License : BSD3
Maintainer : mtesseract@silverratio.net
Stability : experimental
Portability : POSIX
-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecursiveDo #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeSynonymInstances #-}
module Data.Format
( fmt
, fmtConcat
) where
import Control.Applicative
import Control.Exception (SomeException)
import Data.Char
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Lazy as Text.Lazy
import Language.Haskell.Meta.Parse
import Language.Haskell.TH
import Language.Haskell.TH.Quote
import Language.Haskell.TH.Syntax
import Text.Earley
-- | This is just specialized 'mconcat', reexported under a
-- specialized name in order to avoid namespace clashes.
fmtConcat :: [Text] -> Text
fmtConcat = mconcat
-- | Type class which needs to be implemented by types that should be
-- usable for format string interpolation. For most types the this
-- class is simply implemented in terms of 'show'. But for
-- human-readable strings (e.g. 'String', 'Text'), the format
-- representation is simply the string itself, not its 'show'-image
-- (which adds quotation characters).
class Format a where
formatText :: a -> Text
instance Format Int where
formatText = tshow
instance Format SomeException where
formatText = tshow
instance Format String where
formatText = Text.pack
instance Format Double where
formatText = tshow
instance Format Float where
formatText = tshow
instance Format Integer where
formatText = tshow
instance Format Text where
formatText = id
instance Format Text.Lazy.Text where
formatText = Text.Lazy.toStrict
instance Format Bool where
formatText = tshow
tshow :: Show a => a -> Text
tshow = Text.pack . show
data Fmt = Literal String
| Identifier String
| Expression String
deriving (Show, Eq)
-- | Quasi Quoter for format strings. Examples:
--
-- Examples:
--
-- >>> let answer = 42 in [fmt|What is the answer to universe, life and everything? It's $answer!|]
-- "What is the answer to universe, life and everything? It's 42!"
--
-- >>> let toggle = True in [fmt|The toggle is switched ${if toggle then ("on" :: Text) else "off"}|]
-- "The toggle is switched on"
--
-- >>> let timeDelta = 60 in [fmt|Request latency: ${timeDelta}ms|]
-- "Request latency: 60ms"
fmt :: QuasiQuoter
fmt = QuasiQuoter { quoteExp = parseFormatStringQ
, quotePat = undefined
, quoteType = undefined
, quoteDec = undefined
}
instance Lift Fmt where
lift (Literal s) = stringE s
lift (Identifier s) =
lookupValueName s >>= \case
Just v -> (return . formatTextEmbed . VarE) v
Nothing -> fail $ "Not in scope: '" ++ s ++ "'"
lift (Expression s) = either fail (return . formatTextEmbed) (parseExp s)
formatTextEmbed :: Exp -> Exp
formatTextEmbed expr = AppE (VarE 'formatText) expr
newtype FmtString = FmtString [Fmt]
instance Lift FmtString where
lift (FmtString fmts) = do
fmtExprs <- Prelude.mapM lift fmts
return $ AppE (VarE 'fmtConcat) (ListE fmtExprs)
-- | Parse the provided format string as a Template Haskell
-- expression.
parseFormatStringQ :: String -> Q Exp
parseFormatStringQ s =
let parseResult = FmtString (parseFormatString s)
in [| parseResult |]
-- | Parse the provided format string as a list of 'Fmt' values.
parseFormatString :: String -> [Fmt]
parseFormatString s =
case fullParses (parser fmtParser) s of
([], Report { unconsumed = "" }) ->
[]
([uniqueResult], Report { unconsumed = "" }) ->
uniqueResult
_ ->
fail "Parse failure"
-- | Earley parser for the grammar of format strings.
fmtParser :: Grammar r (Prod r String Char [Fmt])
fmtParser = mdo
-- Initial rule.
start <- rule $ interpolationOrLiteral
-- Either parse an interpolation or a non-empty string literal next.
interpolationOrLiteral <- rule $
interpolationThenRest
<|> literalThenRest
-- Parse an interpolation next (either `$foo$` or `${foo}`).
interpolationThenRest <- rule $
interpolationSimpleThenRest
<|> interpolationDelimitedThenRest
-- Parse a simple interpolation next (i.e. `$foo`).
interpolationSimpleThenRest <- rule $
(Identifier <$> interpolationSimple) `apCons` delimLiteralThenRest
<|> (Identifier <$> interpolationSimple) `apCons` interpolationThenRest
<|> (Identifier <$> interpolationSimple) `apCons` pure []
-- Parse a delimited interpolation next (i.e. `${foo}`).
interpolationDelimitedThenRest <- rule $
(Expression <$> interpolationDelimited) `apCons` interpolationOrLiteral
<|> (Expression <$> interpolationDelimited) `apCons` pure []
-- Parse a single character literal which marks the beginning of a
-- string literal and can be used to end a previous simple
-- interpolation (e.g. whitspace, comma).
delimLiteral <- rule $ Literal <$>
(satisfy (\c -> not (identifierChar c) && c /= '$')) `apCons` strChars
-- Parse a string literal next which starts with a delimiting character.
delimLiteralThenRest <- rule $
delimLiteral `apCons` interpolationThenRest
<|> delimLiteral `apCons` (pure [])
-- Parse a string literal next.
literalThenRest <- rule $
(Literal <$> literal) `apCons` pure []
<|> (Literal <$> literal) `apCons` interpolationThenRest
-- Parse a single Haskell variable name next.
identifier <- rule $
satisfy initialIdentifierChar `apCons` many (satisfy identifierChar)
-- Parse a simple interpolation next.
interpolationSimple <- rule $ token '$' *> identifier
-- Parse a delimited interpolation next.
interpolationDelimited <- rule $ token '$' *> token '{' *> expression <* token '}'
-- Parses a single string literal character. Supports escaping.
strChar <- rule $
satisfy (`Prelude.notElem` ['$', '\\'])
<|> token '\\' *> satisfy (const True)
-- Possibly potentially empty string literal.
strChars <- rule $ many strChar
-- Nonempty string literal
literal <- rule $ strChar `apCons` strChars
-- Parse a expression, i.e. something contained between "${" and "}".
expression <- rule $ some (satisfy (/= '}'))
return start
where apCons = liftA2 (:)
-- | Return True if the given character can be part of a Haskell
-- variable name, False otherwise.
identifierChar :: Char -> Bool
identifierChar c = isLower c || isUpper c || c `Prelude.elem` ['\'', '_']
-- | Return True if the given character can be the initial character
-- of a Haskell variable name.
initialIdentifierChar :: Char -> Bool
initialIdentifierChar c = isLower c || c == '_'