packages feed

th-format (empty) → 0.1.0.0

raw patch · 7 files changed

+382/−0 lines, 7 filesdep +Earleydep +basedep +haskell-src-metasetup-changed

Dependencies added: Earley, base, haskell-src-meta, tasty, tasty-hunit, template-haskell, text, th-format

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Moritz Clasmeier (c) 2018++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Author name here nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,30 @@+# th-format [![Hackage version](https://img.shields.io/hackage/v/th-format.svg?label=Hackage)](https://hackage.haskell.org/package/th-format) [![Stackage version](https://www.stackage.org/package/th-format/badge/lts?label=Stackage)](https://www.stackage.org/package/th-format) [![Build Status](https://travis-ci.org/mtesseract/th-format.svg?branch=master)](https://travis-ci.org/mtesseract/th-format)++### About++This is `th-format`, a Haskell package implementing support for format+strings using Template Haskell quasi quoters. It requires the GHC+extension `QuasiQuotes` to be enabled. Parsing is implemented using+Earley.++This package is BSD3 licensed.++### Examples++Using `th-format`, you can use naive variable interpolation instead of+verbosely concatenating strings manually. Thus, instead of++```haskell+putStrLn $ "Client \"" ++ show client ++ "\" has requested resource \"" ++ show resource ++ "\" at date " ++ show date ++ "."+```++one can directly write:++```haskell+putStrLn $ [fmt|Client "$client" has requested resource "$resource" at date $date|]+```++There are currently two supported ways of interpolation:++1. Simple interpolation, as in `[fmt|Variable foo contains $foo|]`.+1. Expression interpolation, as in `[fmt|The toggle is ${if toggle then ("on" :: Text) else "off"}|]`
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ src/Data/Format.hs view
@@ -0,0 +1,214 @@+{-|+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 == '_'
+ test/Data/Format/Test.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes       #-}+{-# LANGUAGE TemplateHaskell   #-}++module Data.Format.Test (tests) where++import           Data.Format+import           Data.Text        (Text)+import           Test.Tasty+import           Test.Tasty.HUnit++tests :: TestTree+tests =+  let foo = "FOO" :: String+      toggle = True+  in testGroup "Tests"+     [ testCase "Empty string" ([fmt||] @=? "")+     , testCase "Single identifier interpolation" ([fmt|$foo|] @=? "FOO")+     , testCase "Multiple identifier interpolation" ([fmt|$foo$foo|] @=? "FOOFOO")+     , testCase "Constant string" ([fmt|Hello, world!|] @=? "Hello, world!")+     , testCase "Constant string w/ escaping" ([fmt|How much \$\$ do you need?|] @=? "How much $$ do you need?")+     , testCase "Constant string w/ escaping" ([fmt|Got \$\$\? :-\\|] @=? "Got $$? :-\\")+     , testCase "Delimited interpolation" ([fmt|${foo}|] @=? "FOO")+     , testCase "Multiple delimited interpolations" ([fmt|${foo}${foo}|] @=? "FOOFOO")+     , testCase "Mixed interpolations" ([fmt|${foo}$foo${foo}|] @=? "FOOFOOFOO")+     , testCase "Single interpolation between text" ([fmt|Hey $foo, how ya doing?|] @=? "Hey FOO, how ya doing?")+     , testCase "Single interpolation between text w/o ws" ([fmt|BAR$foo|] @=? "BARFOO")+     , testCase "Delimited interpolation between text" ([fmt|BAR${foo}BAR|] @=? "BARFOOBAR")+     , testCase "Delimited interpolation between text" ([fmt|BAR${foo}BAR|] @=? "BARFOOBAR")+     , testCase "Escaped simple interpolation" ([fmt|Escaped simple interpolation: \$foo|] @=? "Escaped simple interpolation: $foo")+     , testCase "Escaped delimited interpolation" ([fmt|Escaped delimited interpolation: \${foo}|] @=? "Escaped delimited interpolation: ${foo}")+     , testCase "Simple interpolations delimited by ws" ([fmt|$foo $foo|] @=? "FOO FOO")+     , testCase "Boolean toggle interpolation" (([fmt|${if toggle then ("on" :: Text) else "off"}|] :: Text) @=? "on")+     , testCase "Boolean toggle interpolation negated" (([fmt|${if (not toggle) then ("on" :: Text) else "off"}|] :: Text) @=? "off")+     ]
+ test/Tests.hs view
@@ -0,0 +1,11 @@+import           System.Environment+import           Test.Tasty++import qualified Data.Format.Test++main :: IO ()+main = do+  defaultMain $+    testGroup "Test Suite"+    [ Data.Format.Test.tests+    ]
+ th-format.cabal view
@@ -0,0 +1,60 @@+-- This file has been generated from package.yaml by hpack version 0.20.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: 4eb13b04d84d5d19948270617adaffdadfb358b7dfefe592f205f7eeeb2ff715++name:           th-format+version:        0.1.0.0+synopsis:       Template Haskell based support for format strings+description:    This package implements a Template Haskell quasi quoter for format strings.+category:       Data+homepage:       https://github.com/mtesseract/th-format#readme+bug-reports:    https://github.com/mtesseract/th-format/issues+author:         Moritz Clasmeier+maintainer:     mtesseract@silverratio.net+copyright:      2017-2018 Moritz Clasmeier+license:        BSD3+license-file:   LICENSE+build-type:     Simple+cabal-version:  >= 1.10++extra-source-files:+    README.md++source-repository head+  type: git+  location: https://github.com/mtesseract/th-format++library+  hs-source-dirs:+      src+  ghc-options: -Wall+  build-depends:+      Earley >=0.12.1.0 && <0.13+    , base >=4.7 && <5+    , haskell-src-meta >=0.8.0.2 && <0.9+    , template-haskell >=2.12.0.0 && <2.13+    , text >=1.2.3.0 && <1.3+  exposed-modules:+      Data.Format+  other-modules:+      Paths_th_format+  default-language: Haskell2010++test-suite th-format-test+  type: exitcode-stdio-1.0+  main-is: Tests.hs+  hs-source-dirs:+      test+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      base+    , tasty+    , tasty-hunit+    , text+    , th-format+  other-modules:+      Data.Format.Test+      Paths_th_format+  default-language: Haskell2010