on 2e53b60 help sign in
158 lines 4.6 kB history raw
1{-# LANGUAGE TemplateHaskell #-}
2{-# LANGUAGE CPP #-}
3-- |
4-- Module : Streamly.Internal.Unicode.String
5-- Copyright : (c) 2022 Composewell Technologies
6-- License : BSD-3-Clause
7-- Maintainer : streamly@composewell.com
8-- Stability : experimental
9-- Portability : GHC
10--
11-- Convenient template Haskell quasiquoters to format strings.
13-- Design Notes:
14--
15-- Essential requirements are:
16--
17-- Haskell expression expansion
18-- Newline treatment (continue without introducing a newline)
19-- Indentation treatment
20--
21-- We choose #{expr} for patching a Haskell expression's value in a string. "$"
22-- instead of "#" was another option (like in neat-interpolation package) but
23-- we did not use that to avoid conflict with strings that are used as shell
24-- commands. Another option was to use just "{}" (like in PyF package) but we
25-- did not use that to avoid conflict with "${}" used in shell.
26--
27-- We use a "#" at the end of line to continue the line. We could use a "\"
28-- as well but that may interfere with CPP.
29--
30-- Stripping is not part of the quasiquoter as it can be done by a Haskell
31-- function. Other type of formatting on the Haskell expression can be done
32-- using Haskell functions.
34module Streamly.Internal.Unicode.String
35 (
36 -- * Setup
37 -- | To execute the code examples provided in this module in ghci, please
38 -- run the following commands first.
39 --
40 -- $setup
42 str
43 ) where
46import Control.Applicative (Alternative(..))
47import Control.Exception (displayException)
48import Data.Functor.Identity (runIdentity)
49import Streamly.Internal.Data.Parser (Parser)
51import Language.Haskell.TH
52import Language.Haskell.TH.Quote
54import qualified Streamly.Data.Fold as Fold
55import qualified Streamly.Internal.Data.Parser as Parser
56 (some, many, takeWhile1)
57import qualified Streamly.Data.Stream as Stream (fromList, parse)
58import qualified Streamly.Internal.Unicode.Parser as Parser
60#include "DocTestUnicodeString.hs"
62--------------------------------------------------------------------------------
63-- Parsing
64--------------------------------------------------------------------------------
66data StrSegment
67 = StrText String
68 | StrVar String
69 deriving (Show, Eq)
71haskellIdentifier :: Monad m => Parser Char m String
72haskellIdentifier =
73 let p = Parser.alphaNum <|> Parser.char '\'' <|> Parser.char '_'
74 in Parser.some p Fold.toList
76strParser :: Monad m => Parser Char m [StrSegment]
77strParser = Parser.many content Fold.toList
79 where
81 plainText = StrText <$> Parser.takeWhile1 (/= '#') Fold.toList
82 escHash = StrText . (: []) <$> (Parser.char '#' *> Parser.char '#')
83 lineCont = StrText [] <$ (Parser.char '#' *> Parser.char '\n')
84 var = StrVar <$>
85 ( Parser.char '#'
86 *> Parser.char '{'
87 *> haskellIdentifier
88 <* Parser.char '}'
89 )
90 plainHash = StrText . (: []) <$> Parser.char '#'
92 -- order is important
93 content = plainText <|> escHash <|> lineCont <|> var <|> plainHash
95strSegmentExp :: StrSegment -> Q Exp
96strSegmentExp (StrText text) = stringE text
97strSegmentExp (StrVar name) = do
98 valueName <- lookupValueName name
99 case valueName of
100 Just vn -> varE vn
101 Nothing ->
102 fail
103 $ "str quote: Haskell symbol `" ++ name
104 ++ "` is not in scope"
106strExp :: [StrSegment] -> Q Exp
107strExp xs = appE [| concat |] $ listE $ map strSegmentExp xs
109expandVars :: String -> Q Exp
110expandVars ln =
111 case runIdentity $ Stream.parse strParser (Stream.fromList ln) of
112 Left e ->
113 fail $ "str QuasiQuoter parse error: " ++ displayException e
114 Right x ->
115 strExp x
117-- | A QuasiQuoter that treats the input as a string literal:
118--
119-- >>> [str|x|]
120-- "x"
121--
122-- Any @#{symbol}@ is replaced by the value of the Haskell symbol @symbol@
123-- which is in scope:
124--
125-- >>> x = "hello"
126-- >>> [str|#{x} world!|]
127-- "hello world!"
128--
129-- @##@ means a literal @#@ without the special meaning for referencing
130-- haskell symbols:
131--
132-- >>> [str|##{x} world!|]
133-- "#{x} world!"
134--
135-- A @#@ at the end of line means the line continues to the next line without
136-- introducing a newline character:
137--
138-- >>> :{
139-- [str|hello#
140-- world!|]
141-- :}
142-- "hello world!"
143--
144-- Bugs: because of a bug in parsers, a lone # at the end of input gets
145-- removed.
146--
147str :: QuasiQuoter
148str =
149 QuasiQuoter
150 { quoteExp = expandVars
151 , quotePat = notSupported
152 , quoteType = notSupported
153 , quoteDec = notSupported
154 }
156 where
158 notSupported = error "str: Not supported."