{-| 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 == '_'