string-interpolate (empty) → 0.0.1.0
raw patch · 10 files changed
+926/−0 lines, 10 filesdep +QuickCheckdep +basedep +bytestringsetup-changed
Dependencies added: QuickCheck, base, bytestring, criterion, formatting, haskell-src-meta, hspec, interpolate, quickcheck-instances, quickcheck-text, string-interpolate, template-haskell, text, text-conversions, utf8-string
Files
- CHANGELOG.md +7/−0
- LICENSE +30/−0
- README.md +200/−0
- Setup.hs +2/−0
- bench/Bench.hs +70/−0
- src/lib/Data/String/Interpolate.hs +43/−0
- src/lib/Data/String/Interpolate/Conversion.hs +172/−0
- src/lib/Data/String/Interpolate/Parse.hs +141/−0
- string-interpolate.cabal +79/−0
- test/Spec.hs +182/−0
+ CHANGELOG.md view
@@ -0,0 +1,7 @@+# CHANGELOG++## Unreleased++## v0.0.1.0 (2019-03-10)+++ Initial release
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright William Yao (c) 2019++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,200 @@+# string-interpolate++Haskell having 5 different textual types in common use (String, strict and lazy+Text, strict and lazy ByteString) means that doing any kind of string+manipulation becomes a complicated game of type tetris with constant conversion+back and forth. What if string handling was as simple and easy as it is in+literally any other language?++Behold:++```haskell+showWelcomeMessage :: Text -> Integer -> Text+showWelcomeMessage username visits =+ [i|Welcome to my website, #{username}! You are visitor #{visits}!|]+```++No more needing to `mconcat`, `mappend`, and `(<>)` to glue strings together.+No more having to remember a gajillion different functions for converting+between strict and lazy versions of Text, or having to worry about encoding+between Text <=> ByteString. No more getting bitten by trying to work with+Unicode ByteStrings. It just works!++**string-interpolate** provides a quasiquoter, `i`, that allows you to interpolate+expressions directly into your string. It can produce anything that is an+instance of `IsString`, and can interpolate anything which is an instance of+`Show`.++## Unicode handling++**string-interpolate** handles converting to/from Unicode when converting+String/Text to ByteString and vice versa. Lots of libraries use ByteString to+represent human-readable text, even though this is not safe. There are lots of+useful libraries in the ecosystem that are unfortunately annoying to work with+because of the need to generate ByteStrings containing application-specific info.+Insisting on explicitly converting to/from UTF-8 in these cases and handling+decoding failures adds lots of syntactic noise, when often you can reasonably+assume that a given ByteString will, 95% of the time, contain Unicode text.+So string-interpolate aims to provide reasonable defaults around conversion+between ByteString and real textual types so that developers don't need to+constantly be aware of text encodings.++When converting a String/Text to a ByteString, string-interpolate will+automatically encode it as a sequence of UTF-8 bytes. When converting a+ByteString to String/Text, string-interpolate will assume that the ByteString+contains a UTF-8 string, and convert the characters accordingly. Any invalid+characters in the ByteString will be converted to the Unicode replacement+character � (U+FFFD).++Remember: string-interpolate is not designed for 100% correctness around text+encodings, just for convenience in the most common case. If you absolutely need+to be aware of text encodings and to handle decode failures, take a look at+[text-conversions](https://hackage.haskell.org/package/text-conversions).++## Usage++First things first: add **string-interpolate** to your dependencies:++```yaml+dependencies:+ - string-interpolate+```++and import the quasiquoter and enable `-XQuasiQuotes`:++```haskell+{-# LANGUAGE QuasiQuotes #-}++import Data.String.Interpolate ( i )+```++Wrap anything you want to be interpolated with `#{}`:++```haskell+λ> import Data.Time+λ> now <- getCurrentTime+λ> [i|The current time is #{now}.|] :: String+>>> "The current time is 2019-03-10 18:58:40.573892546 UTC."+```++string-interpolate *must* know what concrete type it's producing; it cannot be+used to generate a `IsString a => a`. If you're using string-interpolate from+GHCi, make sure to add type signatures to toplevel usages!++You can also interpolate arbitrary expressions:++```haskell+λ> [i|Tomorrow's date is #{addDays 1 $ utctDay now}.|] :: String+>>> "Tomorrow's date is 2019-03-11."+```++Backslashes are handled exactly the same way they are in normal Haskell strings.+If you need to put a literal `#{` into your string, prefix the pound symbol with+a backslash:++```haskell+λ> [i|\#{ some inner text }#|] :: String+>>> "#{ some inner text }#"+```++## Comparison to other interpolation libraries++Some other interpolation libraries available:++* [interpolate](https://hackage.haskell.org/package/interpolate)+* [formatting](https://hackage.haskell.org/package/formatting)+* Text.Printf, from base+* [neat-interpolation](https://hackage.haskell.org/package/neat-interpolation)++Of these, Text.Printf isn't exception-safe, and neat-interpolation can only+produce Text values. interpolate and formatting solve the same problem of+providing a general way of interpolating any value, into any kind of text.++### Features++| | string-interpolate | interpolate | formatting |+|------------------------------------------|--------------------|-------------|------------|+| String/Text support | ✅ | ✅ | ✅ |+| ByteString support | ✅ | ✅ | ❌ |+| Can interpolate arbitrary Show instances | ✅ | ✅ | ✅ |+| Unicode-aware | ✅ | ❌ | ✅ |++### Performance++Overall: string-interpolate seems to be on-par with or faster than existing+interpolation libraries for all text cases.++Testing on my local machine, creating small (<100 character) Strings shows+string-interpolate as on-par with interpolate and significantly (> 20X) faster+than formatting:++```+benchmarking Small Strings Bench/string-interpolate+time 8.548 ns (8.543 ns .. 8.554 ns)+ 1.000 R² (1.000 R² .. 1.000 R²)+mean 8.516 ns (8.504 ns .. 8.527 ns)+std dev 35.90 ps (29.23 ps .. 43.63 ps)++benchmarking Small Strings Bench/interpolate+time 9.047 ns (9.042 ns .. 9.052 ns)+ 1.000 R² (1.000 R² .. 1.000 R²)+mean 9.028 ns (9.019 ns .. 9.036 ns)+std dev 27.06 ps (21.88 ps .. 34.20 ps)++benchmarking Small Strings Bench/formatting+time 222.3 ns (221.7 ns .. 223.1 ns)+ 1.000 R² (1.000 R² .. 1.000 R²)+mean 222.1 ns (221.6 ns .. 222.8 ns)+std dev 1.902 ns (1.530 ns .. 2.446 ns)+```++I suspect the poor performance of formatting is caused by using `(++)` to+concatenate Strings instead of ShowS.++Doing the same test, but generating small Text and ByteStrings shows+string-interpolate to be on-par with formatting, and significantly (> 20x) faster+than interpolate.++For Text:++```+benchmarking Small Text Bench/string-interpolate+time 173.7 ns (173.1 ns .. 174.1 ns)+ 1.000 R² (1.000 R² .. 1.000 R²)+mean 173.1 ns (172.8 ns .. 173.5 ns)+std dev 1.230 ns (1.042 ns .. 1.486 ns)++benchmarking Small Text Bench/interpolate+time 4.398 μs (4.371 μs .. 4.425 μs)+ 1.000 R² (1.000 R² .. 1.000 R²)+mean 4.403 μs (4.389 μs .. 4.421 μs)+std dev 54.07 ns (41.42 ns .. 80.40 ns)++benchmarking Small Text Bench/formatting+time 243.1 ns (242.5 ns .. 243.8 ns)+ 1.000 R² (1.000 R² .. 1.000 R²)+mean 242.8 ns (242.4 ns .. 243.4 ns)+std dev 1.665 ns (1.443 ns .. 1.982 ns)+```++For ByteString (formatting doesn't support ByteStrings):++```+benchmarking Small ByteString Bench/string-interpolate+time 297.6 ns (295.8 ns .. 299.8 ns)+ 1.000 R² (0.999 R² .. 1.000 R²)+mean 299.8 ns (298.0 ns .. 302.3 ns)+std dev 7.176 ns (5.417 ns .. 9.651 ns)+variance introduced by outliers: 33% (moderately inflated)++benchmarking Small ByteString Bench/interpolate+time 4.389 μs (4.352 μs .. 4.424 μs)+ 1.000 R² (0.999 R² .. 1.000 R²)+mean 4.374 μs (4.350 μs .. 4.398 μs)+std dev 79.48 ns (66.66 ns .. 96.14 ns)+variance introduced by outliers: 18% (moderately inflated)+```++Here interpolate is the poor performer, with the performance loss caused because+it converts all values to String before using `fromString` to convert them to+the target type.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ bench/Bench.hs view
@@ -0,0 +1,70 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE QuasiQuotes #-}++import Criterion ( bench, bgroup, whnf )+import Criterion.Main ( defaultMain )++import qualified Data.ByteString as B+import qualified Data.Text as T++import qualified "string-interpolate" Data.String.Interpolate as SI+import qualified "interpolate" Data.String.Interpolate.IsString as I+import "formatting" Formatting ( (%) )+import qualified "formatting" Formatting as F+import qualified "formatting" Formatting.ShortFormatters as F++--------------------------------------------------------------------------------+-- Interpolating short Strings+--------------------------------------------------------------------------------++shortStringSI :: String -> String+shortStringSI str = [SI.i|A fine day to die, #{str}.|]++shortStringI :: String -> String+shortStringI str = [I.i|A fine day to die, #{str}.|]++shortStringF :: String -> String+shortStringF = F.formatToString ("A fine day to die, " % F.s % ".")++--------------------------------------------------------------------------------+-- Interpolating short Text+--------------------------------------------------------------------------------++shortTextSI :: T.Text -> T.Text+shortTextSI t = [SI.i|A fine day to die, #{t}.|]++shortTextI :: T.Text -> T.Text+shortTextI t = [I.i|A fine day to die, #{t}.|]++shortTextF :: T.Text -> T.Text+shortTextF = F.sformat ("A fine day to die, " % F.st % ".")++--------------------------------------------------------------------------------+-- Interpolating short ByteString+--------------------------------------------------------------------------------++shortByteStringSI :: B.ByteString -> B.ByteString+shortByteStringSI b = [SI.i|A fine day to die, #{b}.|]++shortByteStringI :: B.ByteString -> B.ByteString+shortByteStringI b = [I.i|A fine day to die, #{b}.|]++main :: IO ()+main = defaultMain+ [ bgroup "Small Strings Bench" $+ [ bench "string-interpolate" $ whnf shortStringSI "William"+ , bench "interpolate" $ whnf shortStringI "William"+ , bench "formatting" $ whnf shortStringF "William"+ ]+ , bgroup "Small Text Bench" $+ [ bench "string-interpolate" $ whnf shortTextSI "William"+ , bench "interpolate" $ whnf shortTextI "William"+ , bench "formatting" $ whnf shortTextF "William"+ ]+ , bgroup "Small ByteString Bench" $+ [ bench "string-interpolate" $ whnf shortByteStringSI "William"+ , bench "interpolate" $ whnf shortByteStringI "William"+ -- "formatting" doesn't support ByteStrings.+ ]+ ]
+ src/lib/Data/String/Interpolate.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE TemplateHaskell #-}++module Data.String.Interpolate+ ( i )+where++import Data.Proxy++import Language.Haskell.Meta.Parse ( parseExp )+import Language.Haskell.TH+import Language.Haskell.TH.Quote ( QuasiQuoter(..) )+import Language.Haskell.TH.Syntax ( returnQ )++import Data.String.Interpolate.Conversion+ ( Builder, InterpSink, Interpolatable, build, finalize, interpolate, ofString )+import Data.String.Interpolate.Parse ( InterpSegment(..), dosToUnix, parseInterpSegments )++i :: QuasiQuoter+i = QuasiQuoter+ { quoteExp = toExp . parseInterpSegments . dosToUnix+ , quotePat = err "pattern"+ , quoteType = err "type"+ , quoteDec = err "declaration"+ }+ where err name = error ("Data.String.Interpolate.i: This QuasiQuoter cannot be used as a " ++ name)++ toExp :: Either String [InterpSegment] -> Q Exp+ toExp parseResult = case parseResult of+ Left msg -> fail $ "Data.String.Interpolate.i: " ++ msg+ Right segs -> emitBuildExp segs++ emitBuildExp :: [InterpSegment] -> Q Exp+ emitBuildExp segs = [|finalize Proxy $(go segs)|]+ where go [] = [|ofString Proxy ""|]+ go (Verbatim str : rest) =+ [|build Proxy (ofString Proxy str) $(go rest)|]+ go (Expression expr : rest) =+ [|build Proxy (interpolate Proxy $(reifyExpression expr)) $(go rest)|]++reifyExpression :: String -> Q Exp+reifyExpression s = case parseExp s of+ Left _ -> fail $ "Data.String.Interpolate.i: parse error in expression: " ++ s+ Right e -> pure e
+ src/lib/Data/String/Interpolate/Conversion.hs view
@@ -0,0 +1,172 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE TypeFamilies #-}++module Data.String.Interpolate.Conversion where++import Data.Maybe ( fromMaybe )+import Data.Monoid ( (<>) )+import Data.Proxy+import Data.String ( IsString, fromString )+import Data.Text.Conversions++import qualified Data.ByteString as B+import qualified Data.ByteString.Builder as LB+import qualified Data.ByteString.Lazy as LB+import qualified Data.Text as T+import qualified Data.Text.Lazy as LT hiding ( singleton )+import qualified Data.Text.Lazy.Builder as LT++import qualified "utf8-string" Data.ByteString.Lazy.UTF8 as LUTF8+import qualified "utf8-string" Data.ByteString.UTF8 as UTF8++import "base" Text.Read ( readMaybe )+import "base" Text.Show ( ShowS, showString )++-- |+-- We wrap the builders in B so that we can add a phantom type parameter.+-- This gives the inner `interpolate's enough information to know where+-- they're going and pick an instance, forcing all the types into lockstep.+newtype B dst a = B { unB :: a }+ deriving (Eq, Show)++-- | Does this type require special behavior when something is interpolated /into/ it?+type family IsCustomSink dst where+ IsCustomSink T.Text = 'True+ IsCustomSink LT.Text = 'True+ IsCustomSink B.ByteString = 'True+ IsCustomSink LB.ByteString = 'True+ IsCustomSink _ = 'False++-- | Something that can be interpolated into.+class IsCustomSink dst ~ flag => InterpSink (flag :: Bool) dst where+ type Builder flag dst :: *++ -- | Meant to be used only for verbatim parts of the interpolation.+ ofString :: Proxy flag -> String -> B dst (Builder flag dst)+ -- |+ -- `build' should be 'in-order'; that is, the left builder comes from+ -- a string on the left, and the right builder comes from a string on the right.+ build :: Proxy flag -> B dst (Builder flag dst) -> B dst (Builder flag dst) -> B dst (Builder flag dst)+ finalize :: Proxy flag -> B dst (Builder flag dst) -> dst++-- |+-- Represents that we can interpolate objects of type src into a an+-- interpolation string that returns type dst.+class InterpSink flag dst => Interpolatable (flag :: Bool) src dst where+ interpolate :: Proxy flag -> src -> B dst (Builder flag dst)++instance (IsCustomSink str ~ 'False, IsString str) => InterpSink 'False str where+ type Builder 'False str = ShowS++ ofString _ = B . showString+ build _ (B f) (B g) = B $ f . g+ finalize _ = fromString . ($ "") . unB++instance InterpSink 'True T.Text where+ type Builder 'True T.Text = LT.Builder++ ofString _ = B . LT.fromString+ build _ (B l) (B r) = B $ l <> r+ finalize _ = LT.toStrict . LT.toLazyText . unB++instance InterpSink 'True LT.Text where+ type Builder 'True LT.Text = LT.Builder++ ofString _ = B . LT.fromString+ build _ (B l) (B r) = B $ l <> r+ finalize _ = LT.toLazyText . unB++instance InterpSink 'True B.ByteString where+ type Builder 'True B.ByteString = LB.Builder++ ofString _ = B . LB.byteString . unUTF8 . convertText+ build _ (B l) (B r) = B $ l <> r+ finalize _ = LB.toStrict . LB.toLazyByteString . unB++instance InterpSink 'True LB.ByteString where+ type Builder 'True LB.ByteString = LB.Builder++ ofString _ = B . LB.lazyByteString . unUTF8 . convertText+ build _ (B l) (B r) = B $ l <> r+ finalize _ = LB.toLazyByteString . unB++instance {-# OVERLAPPABLE #-} (Show src, IsString dst, IsCustomSink dst ~ 'False) => Interpolatable 'False src dst where+ interpolate _ = B . shows+instance {-# OVERLAPS #-} (IsString dst, IsCustomSink dst ~ 'False) => Interpolatable 'False String dst where+ interpolate _ = B . showString+instance {-# OVERLAPS #-} (IsString dst, IsCustomSink dst ~ 'False) => Interpolatable 'False T.Text dst where+ interpolate _ = B . showString . T.unpack+instance {-# OVERLAPS #-} (IsString dst, IsCustomSink dst ~ 'False) => Interpolatable 'False LT.Text dst where+ interpolate _ = B . showString . LT.unpack+instance {-# OVERLAPS #-} (IsString dst, IsCustomSink dst ~ 'False) => Interpolatable 'False B.ByteString dst where+ interpolate _ = B . showString . UTF8.toString+instance {-# OVERLAPS #-} (IsString dst, IsCustomSink dst ~ 'False) => Interpolatable 'False LB.ByteString dst where+ interpolate _ = B . showString . LUTF8.toString++instance {-# OVERLAPPABLE #-} Show src => Interpolatable 'True src T.Text where+ interpolate _ = B . LT.fromString . show+instance {-# OVERLAPS #-} Interpolatable 'True String T.Text where+ interpolate _ = B . LT.fromString+instance {-# OVERLAPS #-} Interpolatable 'True T.Text T.Text where+ interpolate _ = B . LT.fromText+instance {-# OVERLAPS #-} Interpolatable 'True LT.Text T.Text where+ interpolate _ = B . LT.fromLazyText+instance {-# OVERLAPS #-} Interpolatable 'True B.ByteString T.Text where+ interpolate _ = B . bsToTextBuilder+instance {-# OVERLAPS #-} Interpolatable 'True LB.ByteString T.Text where+ interpolate _ = B . lbsToTextBuilder++instance {-# OVERLAPPABLE #-} Show src => Interpolatable 'True src LT.Text where+ interpolate _ = B . LT.fromString . show+instance {-# OVERLAPS #-} Interpolatable 'True String LT.Text where+ interpolate _ = B . LT.fromString+instance {-# OVERLAPS #-} Interpolatable 'True T.Text LT.Text where+ interpolate _ = B . LT.fromText+instance {-# OVERLAPS #-} Interpolatable 'True LT.Text LT.Text where+ interpolate _ = B . LT.fromLazyText+instance {-# OVERLAPS #-} Interpolatable 'True B.ByteString LT.Text where+ interpolate _ = B . bsToTextBuilder+instance {-# OVERLAPS #-} Interpolatable 'True LB.ByteString LT.Text where+ interpolate _ = B . lbsToTextBuilder++instance {-# OVERLAPPABLE #-} Show src => Interpolatable 'True src B.ByteString where+ interpolate _ = B . LB.byteString . unUTF8 . convertText . show+instance {-# OVERLAPS #-} Interpolatable 'True String B.ByteString where+ interpolate _ = B . LB.byteString . unUTF8 . convertText+instance {-# OVERLAPS #-} Interpolatable 'True T.Text B.ByteString where+ interpolate _ = B . LB.byteString . unUTF8 . convertText+instance {-# OVERLAPS #-} Interpolatable 'True LT.Text B.ByteString where+ interpolate _ = B . LB.byteString . unUTF8 . convertText+instance {-# OVERLAPS #-} Interpolatable 'True B.ByteString B.ByteString where+ interpolate _ = B . LB.byteString+instance {-# OVERLAPS #-} Interpolatable 'True LB.ByteString B.ByteString where+ interpolate _ = B . LB.lazyByteString++instance {-# OVERLAPPABLE #-} Show src => Interpolatable 'True src LB.ByteString where+ interpolate _ = B . LB.lazyByteString . unUTF8 . convertText . show+instance {-# OVERLAPS #-} Interpolatable 'True String LB.ByteString where+ interpolate _ = B . LB.lazyByteString . unUTF8 . convertText+instance {-# OVERLAPS #-} Interpolatable 'True T.Text LB.ByteString where+ interpolate _ = B . LB.lazyByteString . unUTF8 . convertText+instance {-# OVERLAPS #-} Interpolatable 'True LT.Text LB.ByteString where+ interpolate _ = B . LB.lazyByteString . unUTF8 . convertText+instance {-# OVERLAPS #-} Interpolatable 'True B.ByteString LB.ByteString where+ interpolate _ = B . LB.byteString+instance {-# OVERLAPS #-} Interpolatable 'True LB.ByteString LB.ByteString where+ interpolate _ = B . LB.lazyByteString++-- |+-- Convert a strict ByteString into a Text `LT.Builder', converting any invalid+-- characters into the Unicode replacement character � (U+FFFD).+bsToTextBuilder :: B.ByteString -> LT.Builder+bsToTextBuilder = UTF8.foldr (\char bldr -> LT.singleton char <> bldr) mempty++-- |+-- Convert a lazy ByteString into a Text `LT.Builder', converting any invalid+-- characters into the Unicode replacement character � (U+FFFD).+lbsToTextBuilder :: LB.ByteString -> LT.Builder+lbsToTextBuilder = LUTF8.foldr (\char bldr -> LT.singleton char <> bldr) mempty
+ src/lib/Data/String/Interpolate/Parse.hs view
@@ -0,0 +1,141 @@+{-# LANGUAGE PackageImports #-}++module Data.String.Interpolate.Parse+ ( InterpSegment(..), parseInterpSegments, dosToUnix )+where++import Data.Char+import qualified "base" Numeric as N++data InterpSegment = Expression String | Verbatim String+ deriving (Eq, Show)++-- |+-- Given the raw input from a quasiquote, parse it into the information+-- we need to output the actual expression.+--+-- Returns an error message if parsing fails.+parseInterpSegments :: String -> Either String [InterpSegment]+parseInterpSegments = go ""+ where go :: String -> String -> Either String [InterpSegment]+ go acc parsee = case parsee of+ "" -> Right [verbatim $ reverse acc]+ '\\':'#':rest -> go ('#':acc) rest+ '#':'{':rest -> case span (/= '}') rest of+ (expr, _:rest') ->+ ((verbatim . reverse) acc :) . (Expression expr :) <$> go "" rest'+ (_, "") -> Left "unterminated #{...} interpolation"+ c:cs -> go (c:acc) cs++ verbatim :: String -> InterpSegment+ verbatim = Verbatim . unescape++dosToUnix :: String -> String+dosToUnix = go+ where go xs = case xs of+ '\r' : '\n' : ys -> '\n' : go ys+ y : ys -> y : go ys+ [] -> []++-- |+-- Haskell 2010 character unescaping, see:+-- <http://www.haskell.org/onlinereport/haskell2010/haskellch2.html#x7-200002.6>+unescape :: String -> String+unescape = go+ where+ go input = case input of+ "" -> ""+ '\\' : 'x' : x : xs | isHexDigit x -> case span isHexDigit xs of+ (ys, zs) -> (chr . readHex $ x:ys) : go zs+ '\\' : 'o' : x : xs | isOctDigit x -> case span isOctDigit xs of+ (ys, zs) -> (chr . readOct $ x:ys) : go zs+ '\\' : x : xs | isDigit x -> case span isDigit xs of+ (ys, zs) -> (chr . read $ x:ys) : go zs+ '\\' : input_ -> case input_ of+ '\\' : xs -> '\\' : go xs+ 'a' : xs -> '\a' : go xs+ 'b' : xs -> '\b' : go xs+ 'f' : xs -> '\f' : go xs+ 'n' : xs -> '\n' : go xs+ 'r' : xs -> '\r' : go xs+ 't' : xs -> '\t' : go xs+ 'v' : xs -> '\v' : go xs+ '&' : xs -> go xs+ 'N':'U':'L' : xs -> '\NUL' : go xs+ 'S':'O':'H' : xs -> '\SOH' : go xs+ 'S':'T':'X' : xs -> '\STX' : go xs+ 'E':'T':'X' : xs -> '\ETX' : go xs+ 'E':'O':'T' : xs -> '\EOT' : go xs+ 'E':'N':'Q' : xs -> '\ENQ' : go xs+ 'A':'C':'K' : xs -> '\ACK' : go xs+ 'B':'E':'L' : xs -> '\BEL' : go xs+ 'B':'S' : xs -> '\BS' : go xs+ 'H':'T' : xs -> '\HT' : go xs+ 'L':'F' : xs -> '\LF' : go xs+ 'V':'T' : xs -> '\VT' : go xs+ 'F':'F' : xs -> '\FF' : go xs+ 'C':'R' : xs -> '\CR' : go xs+ 'S':'O' : xs -> '\SO' : go xs+ 'S':'I' : xs -> '\SI' : go xs+ 'D':'L':'E' : xs -> '\DLE' : go xs+ 'D':'C':'1' : xs -> '\DC1' : go xs+ 'D':'C':'2' : xs -> '\DC2' : go xs+ 'D':'C':'3' : xs -> '\DC3' : go xs+ 'D':'C':'4' : xs -> '\DC4' : go xs+ 'N':'A':'K' : xs -> '\NAK' : go xs+ 'S':'Y':'N' : xs -> '\SYN' : go xs+ 'E':'T':'B' : xs -> '\ETB' : go xs+ 'C':'A':'N' : xs -> '\CAN' : go xs+ 'E':'M' : xs -> '\EM' : go xs+ 'S':'U':'B' : xs -> '\SUB' : go xs+ 'E':'S':'C' : xs -> '\ESC' : go xs+ 'F':'S' : xs -> '\FS' : go xs+ 'G':'S' : xs -> '\GS' : go xs+ 'R':'S' : xs -> '\RS' : go xs+ 'U':'S' : xs -> '\US' : go xs+ 'S':'P' : xs -> '\SP' : go xs+ 'D':'E':'L' : xs -> '\DEL' : go xs+ '^':'@' : xs -> '\^@' : go xs+ '^':'A' : xs -> '\^A' : go xs+ '^':'B' : xs -> '\^B' : go xs+ '^':'C' : xs -> '\^C' : go xs+ '^':'D' : xs -> '\^D' : go xs+ '^':'E' : xs -> '\^E' : go xs+ '^':'F' : xs -> '\^F' : go xs+ '^':'G' : xs -> '\^G' : go xs+ '^':'H' : xs -> '\^H' : go xs+ '^':'I' : xs -> '\^I' : go xs+ '^':'J' : xs -> '\^J' : go xs+ '^':'K' : xs -> '\^K' : go xs+ '^':'L' : xs -> '\^L' : go xs+ '^':'M' : xs -> '\^M' : go xs+ '^':'N' : xs -> '\^N' : go xs+ '^':'O' : xs -> '\^O' : go xs+ '^':'P' : xs -> '\^P' : go xs+ '^':'Q' : xs -> '\^Q' : go xs+ '^':'R' : xs -> '\^R' : go xs+ '^':'S' : xs -> '\^S' : go xs+ '^':'T' : xs -> '\^T' : go xs+ '^':'U' : xs -> '\^U' : go xs+ '^':'V' : xs -> '\^V' : go xs+ '^':'W' : xs -> '\^W' : go xs+ '^':'X' : xs -> '\^X' : go xs+ '^':'Y' : xs -> '\^Y' : go xs+ '^':'Z' : xs -> '\^Z' : go xs+ '^':'[' : xs -> '\^[' : go xs+ '^':'\\' : xs -> '\^\' : go xs+ '^':']' : xs -> '\^]' : go xs+ '^':'^' : xs -> '\^^' : go xs+ '^':'_' : xs -> '\^_' : go xs+ xs -> go xs+ x:xs -> x : go xs++ readHex :: String -> Int+ readHex xs = case N.readHex xs of+ [(n, "")] -> n+ _ -> error "Data.String.Interpolate.Util.readHex: no parse"++ readOct :: String -> Int+ readOct xs = case N.readOct xs of+ [(n, "")] -> n+ _ -> error "Data.String.Interpolate.Util.readHex: no parse"
+ string-interpolate.cabal view
@@ -0,0 +1,79 @@+cabal-version: 1.18++-- This file has been generated from package.yaml by hpack version 0.31.1.+--+-- see: https://github.com/sol/hpack+--+-- hash: e19a345342449e9beb74add05e63adf5a16f1e6899912ae637a1b6f8a4bca51e++name: string-interpolate+version: 0.0.1.0+synopsis: Haskell string interpolation that just works+description: Unicode-aware string interpolation that handles all textual types.+ .+ See the README at <https://gitlab.com/williamyaoh/string-interpolate.git#string-interpolate> for more info.+category: Data, Text+author: William Yao+maintainer: williamyaoh@gmail.com+copyright: 2019 William Yao+license: BSD3+license-file: LICENSE+build-type: Simple+extra-doc-files:+ README.md+ CHANGELOG.md++library+ exposed-modules:+ Data.String.Interpolate+ Data.String.Interpolate.Conversion+ other-modules:+ Data.String.Interpolate.Parse+ Paths_string_interpolate+ hs-source-dirs:+ src/lib+ build-depends:+ base ==4.*+ , bytestring+ , haskell-src-meta+ , template-haskell+ , text+ , text-conversions+ , utf8-string+ default-language: Haskell2010++test-suite string-interpolate-test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Paths_string_interpolate+ hs-source-dirs:+ test+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ QuickCheck+ , base ==4.*+ , bytestring+ , hspec+ , quickcheck-instances+ , quickcheck-text+ , string-interpolate+ , text+ default-language: Haskell2010++benchmark string-interpolate-bench+ type: exitcode-stdio-1.0+ main-is: Bench.hs+ other-modules:+ Paths_string_interpolate+ hs-source-dirs:+ bench+ build-depends:+ base ==4.*+ , bytestring+ , criterion+ , formatting+ , interpolate+ , string-interpolate+ , text+ default-language: Haskell2010
+ test/Spec.hs view
@@ -0,0 +1,182 @@+{-# LANGUAGE QuasiQuotes, ScopedTypeVariables, OverloadedStrings #-}+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE DerivingStrategies #-}++import qualified Data.Text as T+import qualified Data.Text.Lazy as LT+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as LB+import qualified Data.ByteString.Builder as LB+import Data.Foldable ( foldMap )++import "quickcheck-text" Data.Text.Arbitrary++import "hspec" Test.Hspec+import "QuickCheck" Test.QuickCheck+import "quickcheck-instances" Test.QuickCheck.Instances.ByteString++import "string-interpolate" Data.String.Interpolate ( i )++main :: IO ()+main = hspec $ parallel $ do+ describe "i" $ do+ context "when using String as a parameter" $ do+ it "just interpolating should be id" $+ property $ \(str :: String) -> [i|#{str}|] == str++ it "should passthrough a conversion to strict Text and back unchanged" $+ property $ \(str :: String) ->+ let t = [i|#{str}|] :: T.Text+ str' = [i|#{t}|] :: String+ in str' == str++ it "should passthrough a conversion to lazy Text and back unchanged" $+ property $ \(str :: String) ->+ let lt = [i|#{str}|] :: LT.Text+ str' = [i|#{lt}|] :: String+ in str' == str++ it "should passthrough a conversion to strict ByteString and back unchanged" $+ property $ \(str :: String) ->+ let b = [i|#{str}|] :: B.ByteString+ str' = [i|#{b}|] :: String+ in str' == str++ it "should passthrough a conversion to lazy ByteString and back unchanged" $+ property $ \(str :: String) ->+ let lb = [i|#{str}|] :: LB.ByteString+ str' = [i|#{lb}|] :: String+ in str' == str++ context "when using strict Text as a parameter" $ do+ it "just interpolating should be id" $+ property $ \(t :: T.Text) -> [i|#{t}|] == t++ it "should passthrough a conversion to String and back unchanged" $+ property $ \(t :: T.Text) ->+ let str = [i|#{t}|] :: String+ t' = [i|#{str}|] :: T.Text+ in t' == t++ it "should passthrough a conversion to lazy Text and back unchanged" $+ property $ \(t :: T.Text) ->+ let lt = [i|#{t}|] :: LT.Text+ t' = [i|#{lt}|] :: T.Text+ in t' == t++ it "should passthrough a conversion to strict ByteString and back unchanged" $+ property $ \(t :: T.Text) ->+ let b = [i|#{t}|] :: B.ByteString+ t' = [i|#{b}|] :: T.Text+ in t' == t++ it "should passthrough a conversion to lazy ByteString and back unchanged" $+ property $ \(t :: T.Text) ->+ let lb = [i|#{t}|] :: LB.ByteString+ t' = [i|#{lb}|] :: T.Text+ in t' == t++ context "when using lazy Text as a parameter" $ do+ it "just interpolating should be id" $+ property $ \(lt :: LT.Text) -> [i|#{lt}|] == lt++ it "should passthrough a conversion to String and back unchanged" $+ property $ \(lt :: LT.Text) ->+ let str = [i|#{lt}|] :: String+ lt' = [i|#{str}|] :: LT.Text+ in lt' == lt++ it "should passthrough a conversion to strict Text and back unchanged" $+ property $ \(lt :: LT.Text) ->+ let t = [i|#{lt}|] :: T.Text+ lt' = [i|#{t}|] :: LT.Text+ in lt' == lt++ it "should passthrough a conversion to strict ByteString and back unchanged" $+ property $ \(lt :: LT.Text) ->+ let b = [i|#{lt}|] :: B.ByteString+ lt' = [i|#{b}|] :: LT.Text+ in lt' == lt++ it "should passthrough a conversion to lazy ByteString and back unchanged" $+ property $ \(lt :: LT.Text) ->+ let lb = [i|#{lt}|] :: LB.ByteString+ lt' = [i|#{lb}|] :: LT.Text+ in lt' == lt++ context "when using strict ByteString as a parameter" $ do+ it "just interpolating should be id" $+ property $ \(b :: B.ByteString) -> [i|#{b}|] == b++ it "should passthrough a conversion to lazy ByteString and back unchanged" $+ property $ \(b :: B.ByteString) ->+ let lb = [i|#{b}|] :: LB.ByteString+ b' = [i|#{lb}|] :: B.ByteString+ in b' == b++ context "and the ByteString is valid UTF8" $ do+ it "should passthrough a conversion to String and back unchanged" $ do+ property $ \(UTF8 b) ->+ let str = [i|#{b}|] :: String+ b' = [i|#{str}|]+ in b' == b++ it "should passthrough a conversion to strict Text and back unchanged" $ do+ property $ \(UTF8 b) ->+ let t = [i|#{b}|] :: T.Text+ b' = [i|#{t}|]+ in b' == b++ it "should passthrough a conversion to lazy Text and back unchanged" $ do+ property $ \(UTF8 b) ->+ let lt = [i|#{b}|] :: LT.Text+ b' = [i|#{lt}|]+ in b' == b++ context "when using lazy ByteString as a parameter" $ do+ it "just interpolating should be id" $+ property $ \(lb :: LB.ByteString) -> [i|#{lb}|] == lb++ it "should passthrough a conversion to strict ByteString and back unchanged" $+ property $ \(lb :: LB.ByteString) ->+ let b = [i|#{lb}|] :: B.ByteString+ lb' = [i|#{b}|] :: LB.ByteString+ in lb' == lb++ context "and the ByteString is valid UTF8" $ do+ it "should passthrough a conversion to String and back unchanged" $+ property $ \(LUTF8 lb) ->+ let str = [i|#{lb}|] :: String+ lb' = [i|#{str}|]+ in lb' == lb++ it "should passthrough a conversion to strict Text and back unchanged" $+ property $ \(LUTF8 lb) ->+ let t = [i|#{lb}|] :: T.Text+ lb' = [i|#{t}|]+ in lb' == lb++ it "should passthrough a conversion to lazy Text and back unchanged" $+ property $ \(LUTF8 lb) ->+ let lt = [i|#{lb}|] :: LT.Text+ lb' = [i|#{lt}|]+ in lb' == lb++newtype UTF8ByteString = UTF8 { unUTF8 :: B.ByteString }+ deriving newtype (Eq, Show)+newtype LUTF8ByteString = LUTF8 { unLUTF8 :: LB.ByteString }+ deriving newtype (Eq, Show)++instance Arbitrary LUTF8ByteString where+ arbitrary = LUTF8 . LB.toLazyByteString . foldMap LB.charUtf8 <$> arbitrary @[Char]+instance Arbitrary UTF8ByteString where+ arbitrary = UTF8 . LB.toStrict . LB.toLazyByteString . foldMap LB.charUtf8 <$>+ arbitrary @[Char]++-- Why the hell doesn't `quickcheck-text' not provide this+-- instance already? I don't know. Don't ask me.+instance Arbitrary LT.Text where+ arbitrary = LT.fromStrict <$> arbitrary+ shrink t = LT.fromStrict <$> shrink (LT.toStrict t)