├── .gitignore ├── Performance.md ├── README.md ├── Samples.md ├── demo ├── spago.yaml └── src │ ├── Demo.purs │ ├── DemoMore.purs │ └── DemoPerf.purs ├── justfile ├── logo.svg ├── spago.yaml ├── src ├── Fmt.purs └── Fmt │ └── Config.purs └── test └── Test └── Main.purs /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | bower_components/ 3 | node_modules/ 4 | .pulp-cache/ 5 | output/ 6 | output-es/ 7 | generated-docs/ 8 | .psc-package/ 9 | .psc* 10 | .purs* 11 | .psa* 12 | .spago 13 | -------------------------------------------------------------------------------- /Performance.md: -------------------------------------------------------------------------------- 1 | # Runtime performance 2 | 3 | If you use [purs-backend-es](https://github.com/aristanetworks/purescript-backend-optimizer) 4 | you'll get optimized code for free. This is a simple example of how it works. 5 | 6 | For the code in this file, you need those imports: 7 | 8 | 9 | ```hs 10 | module DemoPerf where 11 | 12 | import Fmt (fmt) 13 | ``` 14 | 15 | 16 | ## Static replacements 17 | 18 | A simple replacement of strings known at compile time... 19 | 20 | 21 | ```hs 22 | greeting1 :: String 23 | greeting1 = 24 | fmt 25 | @"Hello, my name is {name}. I live in {city}." 26 | { name: "John" 27 | , city: "London" 28 | } 29 | ``` 30 | 31 | 32 | ...will compile to a plain JS string: 33 | ```js 34 | const greeting1 = "Hello, my name is John. I live in London."; 35 | ``` 36 | 37 | ## Dynamic replacements 38 | 39 | Replacing strings with values not known at compile time... 40 | 41 | 42 | ```hs 43 | greeting2 :: { name :: String, city :: String } -> String 44 | greeting2 = 45 | fmt 46 | @"Hello, my name is {name}. I live in {city}." 47 | ``` 48 | 49 | 50 | ...will compile to simple string concatenation: 51 | ```js 52 | const greeting2 = (fields) => 53 | "Hello, my name is " + fields.name + ". I live in " + fields.city + "."; 54 | ``` 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # purescript-fmt 2 | 3 | Format strings, safely. 4 | 5 | ![fmt](logo.svg) 6 | 7 | ## Installation 8 | 9 | ``` 10 | spago install fmt 11 | ``` 12 | 13 | ## Sample 14 | 15 | 16 | ```hs 17 | module Demo where 18 | 19 | import Fmt (fmt) 20 | 21 | greeting :: String 22 | greeting = 23 | fmt 24 | @""" 25 | Hello, my name is {name}. 26 | I live in {city}. 27 | I am {age} years old. 28 | """ 29 | { name: "Tom" 30 | , city: "London" 31 | , age: 42 32 | } 33 | ``` 34 | 35 | You can [check out more examples here](./Samples.md). 36 | 37 | There's also a guide about [runtime perfomance](./Performance.md). 38 | 39 | ## Features 40 | 41 | - Compile-time format string validation 42 | - Easily extensible with custom formatter type class 43 | - Configurable placeholder syntax 44 | - Zero runtime overhead if used with [purs-backend-es](https://github.com/aristanetworks/purescript-backend-optimizer) 45 | -------------------------------------------------------------------------------- /Samples.md: -------------------------------------------------------------------------------- 1 | ## Imports 2 | 3 | For the code snippets in this document we'll need the following imports: 4 | 5 | 6 | ```hs 7 | module DemoMore where 8 | 9 | import Prelude 10 | 11 | import Data.String as Str 12 | import Fmt (fmt, fmtWith, type (#)) 13 | import Fmt as Fmt 14 | ``` 15 | 16 | ## Zero Config Sample 17 | 18 | 19 | ```hs 20 | greeting1 :: String 21 | greeting1 = 22 | fmt 23 | @""" 24 | Hello, my name is {name}. 25 | I live in {city}. 26 | I am {age} years old. 27 | """ 28 | { name: "Tom" 29 | , city: "London" 30 | , age: 42 31 | } 32 | ``` 33 | 34 | By default you can only use a limited set of types in the replacements: 35 | - `String` 36 | - `Int` 37 | - `Number` 38 | - `Char` 39 | 40 | 41 | ## Sample with simple Config 42 | 43 | In this sample we're overriding the default config to use `<` and `>` as 44 | open/close symbols. 45 | 46 | 47 | ```hs 48 | type MySimpleCfg = 49 | Fmt.DefaultConfig 50 | # Fmt.SetOpenClose "<" ">" 51 | 52 | greeting2 :: String 53 | greeting2 = 54 | fmtWith 55 | @MySimpleCfg 56 | @""" 57 | Hello, my name is . I live in . I am years old. 58 | """ 59 | { name: "Tom" 60 | , city: "London" 61 | , age: 28 62 | } 63 | ``` 64 | 65 | ## Sample with advanced Config 66 | 67 | In this sample we're extending the simple config to use a custom typeclass 68 | for converting different types to strings. 69 | 70 | First we create the typeclass. See the next section for more details about 71 | why you need to provide symbols (like `"int"`) for each type. 72 | 73 | 74 | ```hs 75 | class MyToString a (sym :: Symbol) | a -> sym where 76 | myToString :: a -> String 77 | 78 | instance MyToString Int "int" where 79 | myToString = show 80 | 81 | instance MyToString String "string" where 82 | myToString = identity 83 | 84 | instance MyToString (Array String) "array_string" where 85 | myToString = Str.joinWith ", " 86 | ``` 87 | 88 | Then we create a "dummy type" that we'll use to tell `fmt` to use our typeclass: 89 | 90 | 91 | ```hs 92 | data UseMyToString 93 | 94 | instance 95 | ( MyToString a sym 96 | ) => 97 | Fmt.ToStringBy UseMyToString a sym where 98 | toStringBy _ = myToString 99 | ``` 100 | 101 | Finally we can use our custom typeclass in the template string: 102 | 103 | 104 | ```hs 105 | type MyAdvancedCfg = 106 | Fmt.DefaultConfig 107 | # Fmt.SetOpenClose "<" ">" 108 | # Fmt.SetToString UseMyToString 109 | 110 | 111 | greeting3 :: String 112 | greeting3 = 113 | fmtWith 114 | @MyAdvancedCfg 115 | @""" 116 | Hello, my name is . I live in . 117 | My hobbies are: 118 | """ 119 | { name: "Tom" 120 | , city: "London" 121 | , hobbies: [ "football", "basketball", "swimming" ] 122 | } 123 | ``` 124 | 125 | ## Optionally annotate Replacements with Type Info 126 | 127 | You can als annotate replacements with type info: 128 | 129 | 130 | ```hs 131 | greeting4 :: String 132 | greeting4 = 133 | fmt 134 | @""" 135 | Hello, my name is {name@string}. 136 | I am {age@int} years old. 137 | """ 138 | { name: "Tom" 139 | , age: 42 140 | } 141 | ``` 142 | 143 | This is particularly interesting because now the template string itself 144 | contains all the information about possible replacements. 145 | This information can be leveraged by other external tools 146 | that verify the correctness of the template string. 147 | -------------------------------------------------------------------------------- /demo/spago.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | dependencies: 3 | - console 4 | - effect 5 | - prelude 6 | - record 7 | - strings 8 | - typelevel-prelude 9 | - fmt 10 | name: fmt-demo 11 | test: 12 | dependencies: [] 13 | main: Test.Main 14 | workspace: 15 | extra_packages: 16 | fmt: 17 | path: ../ 18 | package_set: 19 | registry: 40.6.0 20 | pedantic_packages: true 21 | persist_warnings: true 22 | -------------------------------------------------------------------------------- /demo/src/Demo.purs: -------------------------------------------------------------------------------- 1 | {- 2 | # purescript-fmt 3 | 4 | Format strings, safely. 5 | 6 | ![fmt](logo.svg) 7 | 8 | ## Installation 9 | 10 | ``` 11 | spago install fmt 12 | ``` 13 | 14 | ## Sample 15 | -} 16 | 17 | module Demo where 18 | 19 | import Fmt (fmt) 20 | 21 | greeting :: String 22 | greeting = 23 | fmt 24 | @""" 25 | Hello, my name is {name}. 26 | I live in {city}. 27 | I am {age} years old. 28 | """ 29 | { name: "Tom" 30 | , city: "London" 31 | , age: 42 32 | } 33 | 34 | {- 35 | You can [check out more examples here](./Samples.md). 36 | 37 | There's also a guide about [runtime perfomance](./Performance.md). 38 | 39 | ## Features 40 | 41 | - Compile-time format string validation 42 | - Easily extensible with custom formatter type class 43 | - Configurable placeholder syntax 44 | - Zero runtime overhead if used with [purs-backend-es](https://github.com/aristanetworks/purescript-backend-optimizer) 45 | -} -------------------------------------------------------------------------------- /demo/src/DemoMore.purs: -------------------------------------------------------------------------------- 1 | {- 2 | ## Imports 3 | 4 | For the code snippets in this document we'll need the following imports: 5 | -} 6 | 7 | module DemoMore where 8 | 9 | import Prelude 10 | 11 | import Data.String as Str 12 | import Fmt (fmt, fmtWith, type (#)) 13 | import Fmt as Fmt 14 | 15 | {- 16 | ## Zero Config Sample 17 | -} 18 | 19 | greeting1 :: String 20 | greeting1 = 21 | fmt 22 | @""" 23 | Hello, my name is {name}. 24 | I live in {city}. 25 | I am {age} years old. 26 | """ 27 | { name: "Tom" 28 | , city: "London" 29 | , age: 42 30 | } 31 | 32 | {- 33 | By default you can only use a limited set of types in the replacements: 34 | - `String` 35 | - `Int` 36 | - `Number` 37 | - `Char` 38 | 39 | 40 | ## Sample with simple Config 41 | 42 | In this sample we're overriding the default config to use `<` and `>` as 43 | open/close symbols. 44 | -} 45 | 46 | type MySimpleCfg = 47 | Fmt.DefaultConfig 48 | # Fmt.SetOpenClose "<" ">" 49 | 50 | greeting2 :: String 51 | greeting2 = 52 | fmtWith 53 | @MySimpleCfg 54 | @""" 55 | Hello, my name is . I live in . I am years old. 56 | """ 57 | { name: "Tom" 58 | , city: "London" 59 | , age: 28 60 | } 61 | 62 | {- 63 | ## Sample with advanced Config 64 | 65 | In this sample we're extending the simple config to use a custom typeclass 66 | for converting different types to strings. 67 | 68 | First we create the typeclass. See the next section for more details about 69 | why you need to provide symbols (like `"int"`) for each type. 70 | -} 71 | 72 | 73 | class MyToString a (sym :: Symbol) | a -> sym where 74 | myToString :: a -> String 75 | 76 | instance MyToString Int "int" where 77 | myToString = show 78 | 79 | instance MyToString String "string" where 80 | myToString = identity 81 | 82 | instance MyToString (Array String) "array_string" where 83 | myToString = Str.joinWith ", " 84 | 85 | {- 86 | Then we create a "dummy type" that we'll use to tell `fmt` to use our typeclass: 87 | -} 88 | 89 | data UseMyToString 90 | 91 | instance 92 | ( MyToString a sym 93 | ) => 94 | Fmt.ToStringBy UseMyToString a sym where 95 | toStringBy _ = myToString 96 | 97 | 98 | {- 99 | Finally we can use our custom typeclass in the template string: 100 | -} 101 | 102 | type MyAdvancedCfg = 103 | Fmt.DefaultConfig 104 | # Fmt.SetOpenClose "<" ">" 105 | # Fmt.SetToString UseMyToString 106 | 107 | 108 | greeting3 :: String 109 | greeting3 = 110 | fmtWith 111 | @MyAdvancedCfg 112 | @""" 113 | Hello, my name is . I live in . 114 | My hobbies are: 115 | """ 116 | { name: "Tom" 117 | , city: "London" 118 | , hobbies: [ "football", "basketball", "swimming" ] 119 | } 120 | 121 | {- 122 | ## Optionally annotate Replacements with Type Info 123 | 124 | You can als annotate replacements with type info: 125 | -} 126 | 127 | greeting4 :: String 128 | greeting4 = 129 | fmt 130 | @""" 131 | Hello, my name is {name@string}. 132 | I am {age@int} years old. 133 | """ 134 | { name: "Tom" 135 | , age: 42 136 | } 137 | 138 | {- 139 | This is particularly interesting because now the template string itself 140 | contains all the information about possible replacements. 141 | This information can be leveraged by other external tools 142 | that verify the correctness of the template string. 143 | -} -------------------------------------------------------------------------------- /demo/src/DemoPerf.purs: -------------------------------------------------------------------------------- 1 | {- 2 | # Runtime performance 3 | 4 | If you use [purs-backend-es](https://github.com/aristanetworks/purescript-backend-optimizer) 5 | you'll get optimized code for free. This is a simple example of how it works. 6 | 7 | For the code in this file, you need those imports: 8 | -} 9 | 10 | module DemoPerf where 11 | 12 | import Fmt (fmt) 13 | 14 | {- 15 | 16 | ## Static replacements 17 | 18 | A simple replacement of strings known at compile time... 19 | -} 20 | 21 | greeting1 :: String 22 | greeting1 = 23 | fmt 24 | @"Hello, my name is {name}. I live in {city}." 25 | { name: "John" 26 | , city: "London" 27 | } 28 | 29 | {- 30 | 31 | ...will compile to a plain JS string: 32 | ```js 33 | const greeting1 = "Hello, my name is John. I live in London."; 34 | ``` 35 | 36 | ## Dynamic replacements 37 | 38 | Replacing strings with values not known at compile time... 39 | -} 40 | 41 | greeting2 :: { name :: String, city :: String } -> String 42 | greeting2 = 43 | fmt 44 | @"Hello, my name is {name}. I live in {city}." 45 | {- 46 | 47 | ...will compile to simple string concatenation: 48 | ```js 49 | const greeting2 = (fields) => 50 | "Hello, my name is " + fields.name + ". I live in " + fields.city + "."; 51 | ``` 52 | -} -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | gen: 2 | purs-to-md --input-purs demo/src/Demo.purs --output-md README.md 3 | purs-to-md --input-purs demo/src/DemoMore.purs --output-md Samples.md 4 | purs-to-md --input-purs demo/src/DemoPerf.purs --output-md Performance.md 5 | 6 | build-es: 7 | cd demo; spago build -p fmt-demo --backend-args '-g js,corefn' 8 | cd demo; purs-backend-es build 9 | prettier --write output-es/*/index.js -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /spago.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | dependencies: 3 | - arrays: ">=7.2.1 <8.0.0" 4 | - foldable-traversable: ">=6.0.0 <7.0.0" 5 | - prelude: ">=6.0.1 <7.0.0" 6 | - record: ">=4.0.0 <5.0.0" 7 | - strings: ">=6.0.1 <7.0.0" 8 | - tuples: ">=7.0.0 <8.0.0" 9 | - type-equality: ">=4.0.1 <5.0.0" 10 | - typelevel-prelude: ">=7.0.0 <8.0.0" 11 | name: fmt 12 | test: 13 | dependencies: 14 | - console 15 | - effect 16 | main: Test.Main 17 | publish: 18 | version: 0.2.1 19 | license: BSD-3-Clause 20 | location: 21 | githubOwner: thought2 22 | githubRepo: purescript-fmt 23 | workspace: 24 | extra_packages: {} 25 | package_set: 26 | registry: 40.6.0 27 | pedantic_packages: true 28 | persist_warnings: true 29 | -------------------------------------------------------------------------------- /src/Fmt.purs: -------------------------------------------------------------------------------- 1 | -- @inline export parseCons arity=3 2 | -- @inline export parseStart(..).parse arity=3 3 | -- @inline export parseNil(..).parse arity=3 4 | 5 | -- @inline export parseIdCons(..).parseId always 6 | -- @inline export parseIdStart(..).parseId always 7 | -- @inline export parseIdNil(..).parseId always 8 | -- @inline export parseIdAt(..).parseId always 9 | -- @inline export parseIdEnd arity=6 10 | 11 | -- @inline export parseTypeIdCons(..).parseTypeId always 12 | -- @inline export parseTypeIdEnd(..).parseTypeId always 13 | -- @inline export parseTypeIdNil(..).parseTypeId always 14 | 15 | -- @inline export formatCons(..).format arity=1 16 | -- @inline export formatNil(..).format arity=1 17 | 18 | -- @inline export fmt arity=3 19 | -- @inline export fmtWith arity=4 20 | 21 | module Fmt 22 | ( module Export 23 | , fmt 24 | , fmtWith 25 | , class Parse 26 | , class ParseId 27 | , class ParseTypeId 28 | , class ToString 29 | , class ToStringBy 30 | , parse 31 | , parseId 32 | , parseTypeId 33 | , toString 34 | , toStringBy 35 | , EOF 36 | ) where 37 | 38 | import Prelude 39 | 40 | import Data.Symbol (class IsSymbol, reflectSymbol) 41 | import Fmt.Config (class EvalConfigSpec, Config, DefaultConfig, DefaultUseToString, MkConfig) 42 | import Prim.Row as Row 43 | import Prim.Symbol as Sym 44 | import Record as Record 45 | import Type.Equality (class TypeEquals) 46 | import Type.Proxy (Proxy(..)) 47 | import Type.Function (type (#)) as Export 48 | import Fmt.Config (Config, DefaultConfig, SetOpenClose, SetToString) as Export 49 | 50 | fmt 51 | :: forall @sym sym' head tail replace 52 | . Parse DefaultConfig head tail replace 53 | => Sym.Append sym EOF sym' 54 | => Sym.Cons head tail sym' 55 | => replace 56 | -> String 57 | fmt = fmtWith @DefaultConfig @sym 58 | 59 | fmtWith 60 | :: forall @config @sym config' sym' head tail replace 61 | . Parse config' head tail replace 62 | => Sym.Append sym EOF sym' 63 | => Sym.Cons head tail sym' 64 | => EvalConfigSpec config config' 65 | => replace 66 | -> String 67 | fmtWith r = parse 68 | (Proxy :: Proxy config') 69 | (Proxy :: _ head) 70 | (Proxy :: _ tail) 71 | r 72 | "" 73 | 74 | type EOF = "$" 75 | 76 | -------------------------------------------------------------------------------- 77 | --- Parse 78 | -------------------------------------------------------------------------------- 79 | 80 | class 81 | Parse 82 | (config :: Config) 83 | (head :: Symbol) 84 | (tail :: Symbol) 85 | (replace :: Type) 86 | where 87 | parse 88 | :: Proxy config 89 | -> Proxy head 90 | -> Proxy tail 91 | -> replace 92 | -> String 93 | -> String 94 | 95 | instance parseNil :: Parse config EOF "" replace where 96 | parse _ _ _ _ x = x 97 | 98 | else instance parseStart :: 99 | ( Sym.Cons head' tail' tail 100 | , ParseId config head' tail' "" (Record replace) 101 | , TypeEquals config (MkConfig open close useToString) 102 | ) => 103 | Parse (MkConfig open close useToString) open tail (Record replace) 104 | where 105 | parse _ _ _ repl str = 106 | parseId 107 | (Proxy :: Proxy config) 108 | (Proxy :: Proxy head') 109 | (Proxy :: Proxy tail') 110 | (Proxy :: Proxy "") 111 | repl 112 | str 113 | 114 | else instance parseCons :: 115 | ( Sym.Cons head' tail' tail 116 | , Parse config head' tail' (Record replace) 117 | , IsSymbol head 118 | ) => 119 | Parse config head tail (Record replace) 120 | where 121 | parse _ _ _ repl str = 122 | parse 123 | (Proxy :: Proxy config) 124 | (Proxy :: Proxy head') 125 | (Proxy :: Proxy tail') 126 | repl 127 | (str <> reflectSymbol (Proxy :: Proxy head)) 128 | 129 | -------------------------------------------------------------------------------- 130 | --- ParseId 131 | -------------------------------------------------------------------------------- 132 | 133 | class 134 | ParseId 135 | (config :: Config) 136 | (head :: Symbol) 137 | (tail :: Symbol) 138 | (id :: Symbol) 139 | (replace :: Type) 140 | where 141 | parseId 142 | :: Proxy config 143 | -> Proxy head 144 | -> Proxy tail 145 | -> Proxy id 146 | -> replace 147 | -> String 148 | -> String 149 | 150 | instance parseIdNil :: ParseId config EOF "" id replace where 151 | parseId _ _ _ _ _ x = x 152 | 153 | else instance parseIdAt :: 154 | ( Sym.Cons head' tail' tail 155 | , ParseTypeId config head' tail' id "" (Record replace) 156 | ) => 157 | ParseId config "@" tail id (Record replace) 158 | where 159 | parseId _ _ _ _ repl str = parseTypeId 160 | (Proxy :: Proxy config) 161 | (Proxy :: Proxy head') 162 | (Proxy :: Proxy tail') 163 | (Proxy :: Proxy id) 164 | (Proxy :: Proxy "") 165 | repl 166 | str 167 | 168 | else instance parseIdEnd :: 169 | ( Sym.Cons head' tail' tail 170 | , Parse config head' tail' (Record replace) 171 | , IsSymbol id 172 | , Row.Cons id a replace' replace 173 | , ToStringBy useToString a typSym 174 | , TypeEquals config (MkConfig open close useToString) 175 | ) => 176 | ParseId (MkConfig open close useToString) close tail id (Record replace) 177 | where 178 | parseId _ _ _ _ repl str = 179 | let 180 | val :: a 181 | val = Record.get (Proxy :: Proxy id) repl 182 | 183 | valStr :: String 184 | valStr = toStringBy (Proxy :: Proxy useToString) val 185 | in 186 | parse 187 | (Proxy :: Proxy config) 188 | (Proxy :: Proxy head') 189 | (Proxy :: Proxy tail') 190 | repl 191 | (str <> valStr) 192 | 193 | else instance parseIdCons :: 194 | ( Sym.Cons head' tail' tail 195 | , ParseId config head' tail' id' (Record replace) 196 | , Sym.Append id head id' 197 | ) => 198 | ParseId config head tail id (Record replace) 199 | where 200 | parseId _ _ _ _ repl str = 201 | parseId 202 | (Proxy :: Proxy config) 203 | (Proxy :: Proxy head') 204 | (Proxy :: Proxy tail') 205 | (Proxy :: Proxy id') 206 | repl 207 | str 208 | 209 | -------------------------------------------------------------------------------- 210 | --- ParseTypeId 211 | -------------------------------------------------------------------------------- 212 | 213 | class 214 | ParseTypeId 215 | (config :: Config) 216 | (head :: Symbol) 217 | (tail :: Symbol) 218 | (id :: Symbol) 219 | (typeId :: Symbol) 220 | (replace :: Type) 221 | where 222 | parseTypeId 223 | :: Proxy config 224 | -> Proxy head 225 | -> Proxy tail 226 | -> Proxy id 227 | -> Proxy typeId 228 | -> replace 229 | -> String 230 | -> String 231 | 232 | instance parseTypeIdNil :: ParseTypeId config EOF "" id typeId replace where 233 | parseTypeId _ _ _ _ _ _ x = x 234 | 235 | else instance parseTypeIdEnd :: 236 | ( Sym.Cons head' tail' tail 237 | , Parse config head' tail' (Record replace) 238 | , IsSymbol id 239 | , Row.Cons id a replace' replace 240 | , ToStringBy useToString a typeId 241 | , TypeEquals config (MkConfig open close useToString) 242 | ) => 243 | ParseTypeId (MkConfig open close useToString) close tail id typeId (Record replace) 244 | where 245 | parseTypeId _ _ _ _ _ repl str = 246 | let 247 | val :: a 248 | val = Record.get (Proxy :: Proxy id) repl 249 | 250 | valStr :: String 251 | valStr = toStringBy (Proxy :: Proxy useToString) val 252 | in 253 | parse 254 | (Proxy :: Proxy config) 255 | (Proxy :: Proxy head') 256 | (Proxy :: Proxy tail') 257 | repl 258 | (str <> valStr) 259 | 260 | else instance parseTypeIdCons :: 261 | ( Sym.Cons head' tail' tail 262 | , ParseTypeId config head' tail' id typeId' (Record replace) 263 | , Sym.Append typeId head typeId' 264 | ) => 265 | ParseTypeId config head tail id typeId (Record replace) 266 | where 267 | parseTypeId _ _ _ _ _ repl str = 268 | parseTypeId 269 | (Proxy :: Proxy config) 270 | (Proxy :: Proxy head') 271 | (Proxy :: Proxy tail') 272 | (Proxy :: Proxy id) 273 | (Proxy :: Proxy typeId') 274 | repl 275 | str 276 | 277 | -------------------------------------------------------------------------------- 278 | --- ToString 279 | -------------------------------------------------------------------------------- 280 | 281 | class 282 | ToStringBy (tok :: Type) (a :: Type) (sym :: Symbol) 283 | | tok a -> sym 284 | where 285 | toStringBy :: Proxy tok -> a -> String 286 | 287 | instance (ToString a sym) => ToStringBy DefaultUseToString a sym where 288 | toStringBy _ = toString 289 | 290 | class 291 | ToString (a :: Type) (sym :: Symbol) 292 | | a -> sym 293 | where 294 | toString :: a -> String 295 | 296 | instance ToString String "string" where 297 | toString = identity 298 | 299 | instance ToString Int "int" where 300 | toString = show 301 | 302 | instance ToString Number "number" where 303 | toString = show 304 | 305 | instance ToString Char "char" where 306 | toString = show 307 | -------------------------------------------------------------------------------- /src/Fmt/Config.purs: -------------------------------------------------------------------------------- 1 | module Fmt.Config where 2 | 3 | import Prim.Symbol as Sym 4 | import Prim.TypeError (class Fail, Beside, QuoteLabel, Text) 5 | 6 | data DefaultUseToString 7 | 8 | foreign import data SetOpenClose :: Symbol -> Symbol -> Config -> Config 9 | 10 | foreign import data SetToString :: Type -> Config -> Config 11 | 12 | foreign import data Config :: Type 13 | 14 | foreign import data MkConfig :: Symbol -> Symbol -> Type -> Config 15 | 16 | type DefaultConfig = MkConfig "{" "}" DefaultUseToString 17 | 18 | class EvalConfigSpec (spec :: Config) (config :: Config) | spec -> config 19 | 20 | instance 21 | ( EvalConfigSpec tail (MkConfig openX closeX useToString) 22 | , SymbolIsChar open 23 | , SymbolIsChar close 24 | ) => 25 | EvalConfigSpec (SetOpenClose open close tail) (MkConfig open close useToString) 26 | 27 | instance 28 | ( EvalConfigSpec tail (MkConfig open close toStringX) 29 | , SymbolIsChar open 30 | , SymbolIsChar close 31 | ) => 32 | EvalConfigSpec (SetToString useToString tail) (MkConfig open close useToString) 33 | 34 | instance EvalConfigSpec (MkConfig open close useToString) (MkConfig open close useToString) 35 | 36 | -------------------------------------------------------------------------------- 37 | --- SymbolIsChar 38 | -------------------------------------------------------------------------------- 39 | 40 | class SymbolIsChar (sym :: Symbol) 41 | 42 | instance (Fail (Text "Cannot be empty")) => SymbolIsChar "" 43 | 44 | else instance 45 | ( Sym.Cons head tail sym 46 | , SymbolIsChar' head tail 47 | ) => 48 | SymbolIsChar sym 49 | 50 | class SymbolIsChar' (head :: Symbol) (tail :: Symbol) 51 | 52 | instance SymbolIsChar' head "" 53 | 54 | else instance 55 | ( Fail (Beside (QuoteLabel sym) (Text " must be single character")) 56 | , Sym.Append head tail sym 57 | ) => 58 | SymbolIsChar' head tail -------------------------------------------------------------------------------- /test/Test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Effect.Class.Console (log) 7 | 8 | main :: Effect Unit 9 | main = do 10 | log "🍕" 11 | log "You should add some tests." 12 | 13 | --------------------------------------------------------------------------------