th-format-0.1.0.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 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 'mconcat', reexported under a specialized name in
-- order to avoid namespace clashes.
fmtConcat :: Monoid a => [a] -> a
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 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 == '_'