-
Notifications
You must be signed in to change notification settings - Fork 8
/
Perl6.hs
267 lines (242 loc) · 8.13 KB
/
Perl6.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
{-# LANGUAGE TemplateHaskell, TypeSynonymInstances, FlexibleInstances,
UndecidableInstances, OverlappingInstances, MultiParamTypeClasses,
IncoherentInstances
#-}
-- | QuasiQuoter for interpolated strings using Perl 6 syntax.
--
-- The 'q' form does one thing and does it well: It contains a multi-line string with
-- no interpolation at all:
--
-- @
-- {-\# LANGUAGE QuasiQuotes, ExtendedDefaultRules #-}
-- import Text.InterpolatedString.Perl6 (q)
-- foo :: String -- 'Text', 'ByteString' etc also works
-- foo = [q|
--
-- Well here is a
-- multi-line string!
--
-- |]
-- @
--
-- Any instance of the 'IsString' class is permitted.
--
-- The 'qc' form interpolates curly braces: expressions inside {} will be
-- directly interpolated if it's a 'Char', 'String', 'Text' or 'ByteString', or
-- it will have 'show' called if it is not.
--
-- Escaping of '{' is done with backslash.
--
-- For interpolating numeric expressions without an explicit type signature,
-- use the ExtendedDefaultRules lanuage pragma, as shown below:
--
-- @
-- {-\# LANGUAGE QuasiQuotes, ExtendedDefaultRules #-}
-- import Text.InterpolatedString.Perl6 (qc)
-- bar :: String
-- bar = [qc| Well {\"hello\" ++ \" there\"} {6 * 7} |]
-- @
--
-- bar will have the value \" Well hello there 42 \".
--
-- If you want control over how 'show' works on your types, define a custom
-- 'ShowQ' instance:
--
-- For example, this instance allows you to display interpolated lists of strings as
-- a sequence of words, removing those pesky brackets, quotes, and escape sequences.
--
-- @
-- {-\# LANGUAGE FlexibleInstances #-}
-- import Text.InterpolatedString.Perl6 (qc, ShowQ(..))
-- instance ShowQ [String] where
-- showQ = unwords
-- @
--
-- The 'qq' form adds to the 'qc' form with a simple shorthand: '$foo' means '{foo}',
-- namely interpolating a single variable into the string.
--
-- @
-- {-\# LANGUAGE QuasiQuotes, ExtendedDefaultRules #-}
-- import Text.InterpolatedString.Perl6 (qq)
-- baz :: String
-- baz = [qq| Hello, $who |]
-- where
-- who = \"World\"
-- @
--
-- Both 'qc' and 'qq' permit output to any types with both 'IsString' and 'Monoid'
-- instances.
--
-- @
-- {-# LANGUAGE QuasiQuotes, OverloadedStrings #-}
-- import Text.InterpolatedString.Perl6 (qc)
-- import Data.Text (Text)
-- import Data.ByteString.Char8 (ByteString)
-- qux :: ByteString
-- qux = [qc| This will convert {\"Text\" :: Text} to {\"ByteString\" :: ByteString} |]
-- @
--
-- The ability to define custom 'ShowQ' instances is particularly powerful with
-- cascading instances using 'qq'.
--
-- Below is a sample snippet from a script that converts Shape objects into
-- AppleScript suitable for drawing in OmniGraffle:
--
-- @
-- {-\# LANGUAGE QuasiQuotes, ExtendedDefaultRules, NamedFieldPuns, RecordWildCards #-}
-- import Text.InterpolatedString.Perl6
-- @
--
-- @
-- data Shape = Shape
-- { originX :: Int
-- , originY :: Int
-- , width :: Int
-- , height :: Int
-- , stroke :: Stroke
-- , text :: Text
-- }
-- instance ShowQ Shape where
-- showQ Shape{..} = [qq|
-- make new shape at end of graphics with properties
-- \\{ $text, $stroke, _size, $_origin }
-- |]
-- where
-- _size = [qq|size: \{$width, $height}|]
-- _origin = [qq|origin: \{$originX, $originY}|]
-- @
--
-- @
-- data Stroke = StrokeWhite | StrokeNone
-- instance ShowQ Stroke where
-- showQ StrokeNone = \"draws stroke:false\"
-- showQ StrokeWhite = \"stroke color: {1, 1, 1}\"
-- @
--
-- @
-- data Text = Text
-- { txt :: String
-- , color :: Color
-- }
-- instance ShowQ Text where
-- showQ Text{..} = [qq|text: \\{ text: \"$txt\", $color, alignment: center } |]
-- @
--
-- @
-- data Color = Color { red :: Float, green :: Float, blue :: Float }
-- instance ShowQ Color where
-- showQ Color{..} = [qq|color: \{$red, $green, $blue}|]
-- @
--
-- @
-- main :: IO ()
-- main = putStrLn [qq|
-- tell application \"OmniGraffle Professional 5\"
-- tell canvas of front window
-- { makeShape ... }
-- end tell
-- end tell
-- |]
-- @
--
module Text.InterpolatedString.Perl6 (qq, qc, q, ShowQ(..)) where
import qualified Language.Haskell.TH as TH
import Language.Haskell.TH.Quote
import Language.Haskell.Meta.Parse
import GHC.Exts (IsString(..))
import Data.Monoid (Monoid(..))
import Data.ByteString.Char8 as Strict (ByteString, unpack)
import Data.ByteString.Lazy.Char8 as Lazy (ByteString, unpack)
import Data.Text as T (Text, unpack)
import Data.Text.Lazy as LazyT(Text, unpack)
import Data.Char (isAlpha, isAlphaNum)
-- |A class for types that use special interpolation rules.
-- Instances of 'ShowQ' that are also instances of 'IsString' should obey the
-- following law:
--
-- @
-- fromString (showQ s) == s
-- @
--
-- because this library relies on this fact to optimize
-- away needless string conversions.
class ShowQ a where
showQ :: a -> String
instance ShowQ Char where
showQ = (:[])
instance ShowQ String where
showQ = id
instance ShowQ Strict.ByteString where
showQ = Strict.unpack
instance ShowQ Lazy.ByteString where
showQ = Lazy.unpack
instance ShowQ T.Text where
showQ = T.unpack
instance ShowQ LazyT.Text where
showQ = LazyT.unpack
instance Show a => ShowQ a where
showQ = show
-- todo: this should really be rewritten into RULES pragmas, but so far
-- I can't convince GHC to let the rules fire.
class QQ a string where
toQQ :: a -> string
instance IsString s => QQ s s where
toQQ = id
instance (ShowQ a, IsString s) => QQ a s where
toQQ = fromString . showQ
data StringPart = Literal String | AntiQuote String deriving Show
unQC a [] = [Literal (reverse a)]
unQC a ('\\':x:xs) = unQC (x:a) xs
unQC a ('\\':[]) = unQC ('\\':a) []
unQC a ('}':xs) = AntiQuote (reverse a) : parseQC [] xs
unQC a (x:xs) = unQC (x:a) xs
parseQC a [] = [Literal (reverse a)]
parseQC a ('\\':'\\':xs) = parseQC ('\\':a) xs
parseQC a ('\\':'{':xs) = parseQC ('{':a) xs
parseQC a ('\\':[]) = parseQC ('\\':a) []
parseQC a ('{':xs) = Literal (reverse a) : unQC [] xs
parseQC a (x:xs) = parseQC (x:a) xs
unQQ a [] = [Literal (reverse a)]
unQQ a ('\\':x:xs) = unQQ (x:a) xs
unQQ a ('\\':[]) = unQQ ('\\':a) []
unQQ a ('}':xs) = AntiQuote (reverse a) : parseQQ [] xs
unQQ a (x:xs) = unQQ (x:a) xs
parseQQ a [] = [Literal (reverse a)]
parseQQ a ('\\':x:xs) = parseQQ (x:a) xs
parseQQ a ('\\':[]) = parseQQ ('\\':a) []
parseQQ a ('$':x:xs) | x == '_' || isAlpha x =
Literal (reverse a) : AntiQuote (x:pre) : parseQQ [] post
where
(pre, post) = span isIdent xs
parseQQ a ('{':xs) = Literal (reverse a) : unQQ [] xs
parseQQ a (x:xs) = parseQQ (x:a) xs
isIdent '_' = True
isIdent '\'' = True
isIdent x = isAlphaNum x
makeExpr [] = [| mempty |]
makeExpr ((Literal a):xs) = TH.appE [| mappend (fromString a) |]
$ makeExpr xs
makeExpr ((AntiQuote a):xs) = TH.appE [| mappend (toQQ $(reify a)) |]
$ makeExpr xs
reify s =
case parseExp s of
Left s -> TH.report True s >> [| mempty |]
Right e -> return e
-- | QuasiQuoter for interpolating '$var' and '{expr}' into a string literal. The pattern portion is undefined.
qq :: QuasiQuoter
qq = QuasiQuoter (makeExpr . parseQQ [] . filter (/= '\r'))
(error "Cannot use qq as a pattern")
(error "Cannot use qq as a type")
(error "Cannot use qq as a dec")
-- | QuasiQuoter for interpolating '{expr}' into a string literal. The pattern portion is undefined.
qc :: QuasiQuoter
qc = QuasiQuoter (makeExpr . parseQC [] . filter (/= '\r'))
(error "Cannot use qc as a pattern")
(error "Cannot use qc as a type")
(error "Cannot use qc as a dec")
-- | QuasiQuoter for a non-interpolating string literal. The pattern portion is undefined.
q :: QuasiQuoter
q = QuasiQuoter ((\a -> [|fromString a|]) . filter (/= '\r'))
(error "Cannot use q as a pattern")
(error "Cannot use q as a type")
(error "Cannot use q as a dec")