packages feed

scanf-0.2.0.0: src/Text/Scanf/TH.hs

{-# LANGUAGE TemplateHaskell #-}

module Text.Scanf.TH where

import Data.Char (isSpace)

import Language.Haskell.TH
import Language.Haskell.TH.Syntax
import Language.Haskell.TH.Quote

import Text.Scanf.Internal

import Data.Set (Set, empty, insert, singleton)

import Numeric (showHex)

formatString :: String -> Q Exp
formatString ('\\' : s1) = formatString' $ escape s1
formatString s1          = formatString'          s1

formatString' :: String -> Q Exp
formatString' ""         = [|Empty|]
formatString' ('%' : s1) =
  case s1 of
    'c' : s' -> [|char    $(formatString s')|]
    'd' : s' -> [|int     $(formatString s')|]
    'f' : s' -> [|double  $(formatString s')|]
    'l' : s' -> [|integer $(formatString s')|]
    's' : s' -> [|string  $(formatString s')|]
    '[' : s' -> mkScanset s'
    '%' : s' -> [|constant "%" $(formatString s')|]
    _ -> error "Invalid format string"
  where
    mkScanset :: String -> Q Exp
    mkScanset s' = [|scanset set inc $(formatString s'')|]
      where
        set :: Set Char
        inc :: Bool
        s'' :: String

        ((set, s''), inc) = parseScanset s'

        parseScanset :: String -> ((Set Char, String), Bool)
        parseScanset []                = error "Unterminated scanset"
        parseScanset ['^', ']']        = error "Unterminated scanset"
        parseScanset ('^' : ']' : t'') = (parseScanset' (singleton ']') t'', False)
        parseScanset ('^' :       t'') = (parseScanset' empty           t'', False)
        parseScanset (']' :       t'') = (parseScanset' (singleton ']') t'', True)
        parseScanset t''               = (parseScanset' empty           t'', True)

        parseScanset' :: Set Char -> String -> (Set Char, String)
        parseScanset' s ('\\' : t'') = parseScanset'' s $ escape t''
        parseScanset' s t''          = parseScanset'' s   t''

        parseScanset'' :: Set Char -> String -> (Set Char, String)
        parseScanset'' _ []                        = error "Unterminated scanset"
        parseScanset'' s (']' :               t'') = (s, t'')
        parseScanset'' s (c1  : '-'  : ']'  : t'') = (insert '-' $ insert c1 s , t'')
        parseScanset'' s (c1  : '-'  : c2   : t'') =
          case c2 of
            '\\' -> parseScanset' (foldr insert s [c1..c2'])  t'''
            _    -> parseScanset' (foldr insert s [c1..c2])   t''
          where
            (c2',t''') = escape' t''
        parseScanset'' s (c   :               t'') = parseScanset' (insert c s)               t''

formatString' s@(c : _) | isSpace c =
  let (s0, s') = span isSpace s
  in [|whitespace $(lift s0) $(formatString s')|]
formatString' s =
  let (s0, s') = break (\c -> isSpace c || c == '%' || c == '\\') s
  in [|constant $(lift s0) $(formatString s')|]

-- Escapes the first character(s) in the input string.
escape :: String -> String
escape s = c : s'
  where (c,s') = escape' s

escape' :: String -> (Char,String)
escape' ""      = error "Can't escape an empty string"
escape' (c : s) =
  case c of
    '0'  -> (toEnum 0x00 , s)
    'a'  -> (toEnum 0x07 , s)
    'b'  -> (toEnum 0x08 , s)
    'e'  -> (toEnum 0x1b , s)
    'f'  -> (toEnum 0x0c , s)
    'n'  -> ('\n'        , s)
    'r'  -> (toEnum 0x0d , s)
    't'  -> ('\t'        , s)
    'v'  -> (toEnum 0x0b , s)
    'x'  -> escapeHex      s
    '\'' -> ('\''        , s)
    '"'  -> ('\"'        , s)
    '?'  -> (toEnum 0x3f , s)
    '\\' -> ('\\'        , s)
    _    -> error $ "Unknown escape sequence " ++ "(0x" ++ showHex (fromEnum c) "): '" ++ [c] ++ "'"
  where
    -- Replaces two hex digits with a character of that value.
    escapeHex :: String -> (Char,String)
    escapeHex (c1 : c0 : t) = ((toEnum $ 16 * fromHexDigit c1 + fromHexDigit c0) , t)
      where
        fromHexDigit :: Char -> Int
        fromHexDigit hc | '0' <= hc && hc <= '9' = fromEnum hc - fromEnum '0'
                        | 'A' <= hc && hc <= 'F' = 10 + fromEnum hc - fromEnum 'A'
                        | 'a' <= hc && hc <= 'f' = 10 + fromEnum hc - fromEnum 'a'
                        | otherwise = error $ "Invalid hex digit in \\x sequence: " ++ [hc]
    escapeHex _ = error "Missing 2-digit hexadecimal number following \\x."

-- | Parse a typed 'Format' string.
--
-- The following conversion strings are supported:
--
-- - @%d@: signed integer ('Int')
-- - @%l@: signed integer ('Integer', unbounded)
-- - @%f@: floating point ('Double')
-- - @%s@: string of non-space characters ('String')
-- - @%c@: single character ('Char')
-- - @%%@: parse/print a literal percent character
--
-- N.B.: in 'scanf', spaces in the format string match any number of whitespace
-- character until the next nonspace character.
--
-- @
-- ['fmt'|%d lazy %s and %d strict %s|]
--   :: 'Format' ('Int' ':+' 'String' ':+' 'Int' ':+' 'String' ':+' ())
-- @
fmt :: QuasiQuoter
fmt = QuasiQuoter
  { quoteExp = formatString
  , quotePat = undefined
  , quoteType = undefined
  , quoteDec = undefined
  }