{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE QuasiQuotes #-} module Text.Printf.TH (s, st, lt, sb, lb, sP, stP, ltP, sbP, lbP) where import Control.Applicative import Control.Monad.IO.Class import Data.Attoparsec.Text hiding (space) import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as LB import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Lazy.Char8 as LB8 import Data.Char hiding (Space) import Data.Data import Data.Maybe import Data.Monoid import Data.String import Data.Text (pack, unpack) import qualified Data.Text as T import qualified Data.Text.IO as T import qualified Data.Text.Lazy as LT import qualified Data.Text.Lazy.IO as LT import Data.Word import Language.Haskell.TH import Language.Haskell.TH.Quote import Numeric import Prelude hiding (lex) import Text.ParserCombinators.ReadP (readP_to_S) import Text.Read.Lex data Specifier = SignedDec | Octal | UnsignedHex | UnsignedHexUpper | FloatS | FloatUpper | Sci | SciUpper | ShorterFloat | ShorterFloatUpper | CharS | Str | Percent | Showable deriving (Eq, Show, Data, Typeable) data Flag = Minus | Plus | Space | Hash | Zero deriving (Eq, Show, Data, Typeable) data Width = Width Integer | WidthStar deriving (Data, Show, Typeable, Eq) data Precision = Precision Integer | PrecisionStar deriving (Data, Show, Typeable, Eq) data Chunk = Chunk { flags :: [Flag] , width :: Maybe Width , precision :: Maybe Precision , spec :: Specifier } | Plain String deriving (Data, Show, Typeable) quoterOfType :: Name -> Bool -> QuasiQuoter quoterOfType m b = QuasiQuoter { quoteExp = \s' -> let lexed = readP_to_S lex $ '"' : concatMap escape s' ++ "\"" escape '"' = "\\\"" escape m' = [m'] in case lexed of [(String str,"")] -> case parseOnly formatP (pack str) of Right r -> chunksToFormatter r m b Left m' -> error $ "Error when parsing format string: " ++ show m' _ -> error "Error when parsing format string" , quotePat = error "printf cannot be used in pattern context" , quoteType = error "printf cannot be used in type context" , quoteDec = error "printf cannot be used in declaration context" } s, st, lt, sb, lb, sP, stP, ltP, sbP, lbP :: QuasiQuoter s = quoterOfType ''String False st = quoterOfType ''T.Text False lt = quoterOfType ''LT.Text False sb = quoterOfType ''B.ByteString False lb = quoterOfType ''LB.ByteString False sP = quoterOfType ''String True stP = quoterOfType ''T.Text True ltP = quoterOfType ''LT.Text True sbP = quoterOfType ''B.ByteString True lbP = quoterOfType ''LB.ByteString True formatP :: Parser [Chunk] formatP = many1 ( char '%' *> chunkP <|> fmap (Plain . unpack) (takeWhile1 (/= '%')) ) <* endOfInput chunkP :: Parser Chunk chunkP = do f <- many flagP w <- option Nothing (Just <$> widthP) p <- option Nothing (Just <$> precisionP) m <- specP return $ Chunk f w p m flagP :: Parser Flag flagP = Minus <$ char '-' <|> Plus <$ char '+' <|> Space <$ char ' ' <|> Hash <$ char '#' <|> Zero <$ char '0' widthP :: Parser Width widthP = ( Width <$> decimal <|> WidthStar <$ char '*' ) precisionP :: Parser Precision precisionP = char '.' *> ( Precision <$> decimal <|> PrecisionStar <$ char '*' ) specP :: Parser Specifier specP = SignedDec <$ (char 'd' <|> char 'i') <|> Octal <$ char 'o' <|> UnsignedHex <$ char 'x' <|> UnsignedHexUpper <$ char 'X' <|> FloatS <$ char 'f' <|> FloatUpper <$ char 'F' <|> Sci <$ char 'e' <|> SciUpper <$ char 'E' <|> ShorterFloat <$ char 'g' <|> ShorterFloatUpper <$ char 'G' <|> CharS <$ char 'c' <|> Str <$ char 's' <|> Percent <$ char '%' <|> Showable <$ char '?' data PrintfArg = PrintfArg { paSpec :: Chunk , widthArg :: Maybe Name , precArg :: Maybe Name , valArg :: Maybe Name } deriving Show collectArgs :: PrintfArg -> [PatQ] collectArgs (PrintfArg _ n1 n2 n3) = map varP $ catMaybes [n1, n2, n3] chunksToFormatter :: [Chunk] -> Name -> Bool -> ExpQ chunksToFormatter cs ty pr = do ns <- mapM argify cs let processor = if pr then [e|output|] else [e|id|] lamE (concatMap collectArgs ns) [e|$(processor) (mconcat $(listE $ map arg ns) :: $(conT ty))|] where argify p@Plain{} = return $ PrintfArg p Nothing Nothing Nothing argify c@Chunk{spec = Percent} = return $ PrintfArg c Nothing Nothing Nothing argify c@Chunk{width = w, precision = p} = do wa <- if w == Just WidthStar then Just <$> newName "a" else return Nothing pa <- if p == Just PrecisionStar then Just <$> newName "a" else return Nothing q' <- newName "a" return $ PrintfArg c wa pa (Just q') q :: Data a => a -> Q Exp q = dataToExpQ (const Nothing) arg :: PrintfArg -> ExpQ arg PrintfArg{paSpec = Plain str} = stringE str arg PrintfArg{paSpec = Chunk{spec = Percent}} = stringE "%" arg c@PrintfArg{valArg = Just v} = (\n -> dispatch n c v) $ case spec $ paSpec c of SignedDec -> 'showIntegral Octal -> 'showOctal UnsignedHex -> 'showHexP UnsignedHexUpper -> 'showUpperHex FloatS -> 'showFloatP FloatUpper -> 'showUpperFloat Sci -> 'showSci SciUpper -> 'showUpperSci ShorterFloat -> 'showShorter ShorterFloatUpper -> 'showUpperShorter CharS -> 'showCharP Str -> 'showStringP Showable -> 'showShowP m -> error $ "Unhandled specifier: " ++ show m arg m = error $ "Unhandled argument: " ++ show m dispatch :: Name -> PrintfArg -> Name -> ExpQ dispatch s' n v = appE (varE 'fromString) $ foldl1 appE [ varE s' , q $ paSpec n , normalize True (widthArg n) , normalize False (precArg n) , varE v ] where normalize b v' = case v' of Nothing -> litE . integerL $ if b then calcWidth $ paSpec n else calcPrec $ paSpec n Just q' -> varE q' showIntegralBasic :: Chunk -- ^ options -> Integer -- ^ width -> Bool -- ^ less than 0? -> String -- ^ prefix -> String -- ^ value -> String showIntegralBasic c w b pre n = space c . plus b c . prefix pre c . pad w c $ n showIntegral :: (Show a, Integral a) => Chunk -> Integer -> Integer -> a -> String showIntegral pa w _ n = showIntegralBasic pa w (n >= 0) "" $ show n showOctal :: (Show a, Integral a) => Chunk -> Integer -> Integer -> a -> String showOctal pa w _ n = showIntegralBasic pa w (n >= 0) "0" $ showOct n "" showHexP :: (Show a, Integral a) => Chunk -> Integer -> Integer -> a -> String showHexP pa w _ n = showIntegralBasic pa w (n >= 0) "0x" $ showHex n "" showUpperHex :: (Show a, Integral a) => Chunk -> Integer -> Integer -> a -> String showUpperHex pa w _ n = showIntegralBasic pa w (n >= 0) "0X" . map toUpper $ showHex n "" showFloatP :: RealFloat a => Chunk -> Integer -> Integer -> a -> String showFloatP pa w pr n = plus (n >= 0) pa . padDelim '.' w pa $ showFFloat (if pr < 0 then Nothing else Just $ fromIntegral pr) n "" showUpperFloat :: RealFloat a => Chunk -> Integer -> Integer -> a -> String showUpperFloat pa w pr n = map toUpper $ showFloatP pa w pr n showSci :: RealFloat a => Chunk -> Integer -> Integer -> a -> String showSci pa w pr n = plus (n >= 0) pa . padDelim '.' w pa $ showEFloat (if pr < 0 then Nothing else Just $ fromIntegral pr) n "" showUpperSci :: RealFloat a => Chunk -> Integer -> Integer -> a -> String showUpperSci pa w pr n = map toUpper $ showSci pa w pr n showShorter :: RealFloat a => Chunk -> Integer -> Integer -> a -> String showShorter pa w pr n = if length f > length e then e else f where f = showFloatP pa w pr n e = showSci pa w pr n showUpperShorter :: RealFloat a => Chunk -> Integer -> Integer -> a -> String showUpperShorter pa w pr n = if length f > length e then e else f where f = showUpperFloat pa w pr n e = showUpperSci pa w pr n showCharP :: ToChar a => Chunk -> Integer -> Integer -> a -> String showCharP _ _ _ c = [asChar c] showStringP :: ToString a => Chunk -> Integer -> Integer -> a -> String showStringP pa w _ n = space pa . pad w pa $ toString n showShowP :: Show a => Chunk -> Integer -> Integer -> a -> String showShowP pa w _ n = space pa . pad w pa $ show n space :: Chunk -> String -> String space c = if Space `elem` flags c && Plus `notElem` flags c then (' ':) else id plus :: Bool -> Chunk -> String -> String plus b c = if Plus `elem` flags c then if b then ('+':) else ('-':) else id prefix :: String -> Chunk -> String -> String prefix s' p = if Hash `elem` flags p then (s' ++) else id padDelim :: Integral a => Char -> a -> Chunk -> String -> String padDelim c w pa s' = a (replicate (fromIntegral w - len) c') s' where len = length $ Prelude.takeWhile (/=c) s' a = if Minus `elem` flags pa then flip (++) else (++) c' = if Zero `elem` flags pa then '0' else ' ' pad :: Integral a => a -> Chunk -> String -> String pad w pa s' = a (replicate (fromIntegral w - length s') c) s' where a = if Minus `elem` flags pa then flip (++) else (++) c = if Zero `elem` flags pa then '0' else ' ' calcWidth :: Chunk -> Integer calcWidth (Chunk _ (Just (Width n)) _ _) = n calcWidth _ = -1 calcPrec :: Chunk -> Integer calcPrec (Chunk _ _ (Just (Precision n)) _) = n calcPrec _ = -1 class ToString a where toString :: a -> String instance ToChar a => ToString [a] where toString = map asChar instance ToString T.Text where toString = T.unpack instance ToString LT.Text where toString = LT.unpack instance ToString B.ByteString where toString = map asChar . B.unpack instance ToString LB.ByteString where toString = map asChar . LB.unpack class ToChar m where asChar :: m -> Char instance ToChar Char where asChar = id instance ToChar Int where asChar = chr instance ToChar Word8 where asChar = chr . fromIntegral class Printable a where output :: MonadIO m => a -> m () instance Printable String where output = liftIO . putStrLn instance Printable T.Text where output = liftIO . T.putStrLn instance Printable LT.Text where output = liftIO . LT.putStrLn instance Printable B.ByteString where output = liftIO . B8.putStrLn instance Printable LB.ByteString where output = liftIO . LB8.putStrLn