├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── flake.lock ├── flake.nix ├── package.nix ├── src └── Substitute.purs └── test └── Main.purs /.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /generated-docs/ 6 | /.psc-package/ 7 | /.psc* 8 | /.purs* 9 | /.psa* 10 | /.spago 11 | /index.js 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Mason Mackaman 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. 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. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | 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 HOLDER 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. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Substitute 2 | 3 | [![Pursuit](https://pursuit.purescript.org/packages/purescript-substitute/badge)](https://pursuit.purescript.org/packages/purescript-substitute) 4 | 5 | Substitute is an advanced string interpolation library that allows you to customize the behaviour of your interpolation function. It includes features such as removing whitespace from multi-line strings so they can be indented like the rest of your code, and preserving indentation levels when inserting multi-line strings. Here is an example showing both of these features. 6 | ```purescript 7 | -- str1 = str2 8 | 9 | str1 = 10 | substitute 11 | """ 12 | ${name} :: Int -> Int -> ${type} 13 | ${name} a b = 14 | let 15 | ${lets} 16 | in 17 | c - d 18 | """ 19 | { name: "myFunction" 20 | , "type": "Int" 21 | , lets: 22 | """ 23 | c = a + b 24 | 25 | d = a * b 26 | """ 27 | } 28 | 29 | str2 = 30 | """myFunction :: Int -> Int -> Int 31 | myFunction a b = 32 | let 33 | c = a + b 34 | 35 | d = a * b 36 | in 37 | c - d 38 | """ 39 | ``` 40 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "purescript-foldable-traversable": "^v6.0.0", 4 | "purescript-foreign-object": "^v4.0.0", 5 | "purescript-maybe": "^v6.0.0", 6 | "purescript-prelude": "^v6.0.0", 7 | "purescript-return": "https://github.com/ursi/purescript-return.git#v0.2.0", 8 | "purescript-strings": "^v6.0.0" 9 | }, 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "output" 15 | ], 16 | "license": [ 17 | "BSD-3-Clause" 18 | ], 19 | "name": "purescript-substitute", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/ursi/purescript-substitute.git" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "builders": { 4 | "locked": { 5 | "lastModified": 1646266317, 6 | "narHash": "sha256-sO7lFbuhK2t13elpWgNB+Nyo40qQOUnX4FwJ+3Ao9ZE=", 7 | "owner": "ursi", 8 | "repo": "nix-builders", 9 | "rev": "72e9cc5487b07d152e3f5f6ed0654bbc7086e6b4", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "ursi", 14 | "repo": "nix-builders", 15 | "type": "github" 16 | } 17 | }, 18 | "deadnix": { 19 | "inputs": { 20 | "mozillapkgs": "mozillapkgs", 21 | "naersk": "naersk", 22 | "nixpkgs": "nixpkgs_3", 23 | "utils": "utils" 24 | }, 25 | "locked": { 26 | "lastModified": 1638932743, 27 | "narHash": "sha256-QVqT8wT/vwU7RHyCn2IOZc+9UPX9E3AE1WOFnbbEywc=", 28 | "owner": "astro", 29 | "repo": "deadnix", 30 | "rev": "600a0da9727ab5b26c79bb33704357d33855da1d", 31 | "type": "github" 32 | }, 33 | "original": { 34 | "owner": "astro", 35 | "repo": "deadnix", 36 | "type": "github" 37 | } 38 | }, 39 | "easy-ps": { 40 | "flake": false, 41 | "locked": { 42 | "lastModified": 1651400038, 43 | "narHash": "sha256-bwbpXSTD8Hf7tlCXfZuLfo2QivvX1ZDJ1PijXXRTo3Q=", 44 | "owner": "justinwoo", 45 | "repo": "easy-purescript-nix", 46 | "rev": "0ad5775c1e80cdd952527db2da969982e39ff592", 47 | "type": "github" 48 | }, 49 | "original": { 50 | "owner": "justinwoo", 51 | "repo": "easy-purescript-nix", 52 | "type": "github" 53 | } 54 | }, 55 | "flake-utils": { 56 | "locked": { 57 | "lastModified": 1618217525, 58 | "narHash": "sha256-WGrhVczjXTiswQaoxQ+0PTfbLNeOQM6M36zvLn78AYg=", 59 | "owner": "numtide", 60 | "repo": "flake-utils", 61 | "rev": "c6169a2772643c4a93a0b5ac1c61e296cba68544", 62 | "type": "github" 63 | }, 64 | "original": { 65 | "owner": "numtide", 66 | "repo": "flake-utils", 67 | "type": "github" 68 | } 69 | }, 70 | "flake-utils_2": { 71 | "locked": { 72 | "lastModified": 1618217525, 73 | "narHash": "sha256-WGrhVczjXTiswQaoxQ+0PTfbLNeOQM6M36zvLn78AYg=", 74 | "owner": "numtide", 75 | "repo": "flake-utils", 76 | "rev": "c6169a2772643c4a93a0b5ac1c61e296cba68544", 77 | "type": "github" 78 | }, 79 | "original": { 80 | "owner": "numtide", 81 | "repo": "flake-utils", 82 | "type": "github" 83 | } 84 | }, 85 | "make-shell": { 86 | "locked": { 87 | "lastModified": 1634940815, 88 | "narHash": "sha256-P69OmveboXzS+es1vQGS4bt+ckwbeIExqxfGLjGuJqA=", 89 | "owner": "ursi", 90 | "repo": "nix-make-shell", 91 | "rev": "8add91681170924e4d0591b22f294aee3f5516f9", 92 | "type": "github" 93 | }, 94 | "original": { 95 | "owner": "ursi", 96 | "ref": "1", 97 | "repo": "nix-make-shell", 98 | "type": "github" 99 | } 100 | }, 101 | "make-shell_2": { 102 | "locked": { 103 | "lastModified": 1634940815, 104 | "narHash": "sha256-P69OmveboXzS+es1vQGS4bt+ckwbeIExqxfGLjGuJqA=", 105 | "owner": "ursi", 106 | "repo": "nix-make-shell", 107 | "rev": "8add91681170924e4d0591b22f294aee3f5516f9", 108 | "type": "github" 109 | }, 110 | "original": { 111 | "owner": "ursi", 112 | "ref": "1", 113 | "repo": "nix-make-shell", 114 | "type": "github" 115 | } 116 | }, 117 | "mozillapkgs": { 118 | "flake": false, 119 | "locked": { 120 | "lastModified": 1637337116, 121 | "narHash": "sha256-LKqAcdL+woWeYajs02bDQ7q8rsqgXuzhC354NoRaV80=", 122 | "owner": "mozilla", 123 | "repo": "nixpkgs-mozilla", 124 | "rev": "cbc7435f5b0b3d17b16fb1d20cf7b616eec5e093", 125 | "type": "github" 126 | }, 127 | "original": { 128 | "owner": "mozilla", 129 | "repo": "nixpkgs-mozilla", 130 | "type": "github" 131 | } 132 | }, 133 | "naersk": { 134 | "inputs": { 135 | "nixpkgs": "nixpkgs_2" 136 | }, 137 | "locked": { 138 | "lastModified": 1638203339, 139 | "narHash": "sha256-Sz3iCvbWrVWOD/XfYQeRJgP/7MVYL3/VKsNXvDeWBFc=", 140 | "owner": "nmattia", 141 | "repo": "naersk", 142 | "rev": "c3e56b8a4ffb6d906cdfcfee034581f9a8ece571", 143 | "type": "github" 144 | }, 145 | "original": { 146 | "owner": "nmattia", 147 | "repo": "naersk", 148 | "type": "github" 149 | } 150 | }, 151 | "nixpkgs": { 152 | "locked": { 153 | "lastModified": 1653117584, 154 | "narHash": "sha256-5uUrHeHBIaySBTrRExcCoW8fBBYVSDjDYDU5A6iOl+k=", 155 | "owner": "NixOS", 156 | "repo": "nixpkgs", 157 | "rev": "f4dfed73ee886b115a99e5b85fdfbeb683290d83", 158 | "type": "github" 159 | }, 160 | "original": { 161 | "owner": "NixOS", 162 | "ref": "nixpkgs-unstable", 163 | "repo": "nixpkgs", 164 | "type": "github" 165 | } 166 | }, 167 | "nixpkgs_2": { 168 | "locked": { 169 | "lastModified": 1638722875, 170 | "narHash": "sha256-B1BSlq6Mg4WLw7eLLW/JCM8xPkNsIkKRYgzJz6YPtEY=", 171 | "owner": "NixOS", 172 | "repo": "nixpkgs", 173 | "rev": "4722a8e10edcf46aaeb0b9f887bb756e25c6930e", 174 | "type": "github" 175 | }, 176 | "original": { 177 | "id": "nixpkgs", 178 | "type": "indirect" 179 | } 180 | }, 181 | "nixpkgs_3": { 182 | "locked": { 183 | "lastModified": 1638722875, 184 | "narHash": "sha256-B1BSlq6Mg4WLw7eLLW/JCM8xPkNsIkKRYgzJz6YPtEY=", 185 | "owner": "NixOS", 186 | "repo": "nixpkgs", 187 | "rev": "4722a8e10edcf46aaeb0b9f887bb756e25c6930e", 188 | "type": "github" 189 | }, 190 | "original": { 191 | "id": "nixpkgs", 192 | "type": "indirect" 193 | } 194 | }, 195 | "nixpkgs_4": { 196 | "locked": { 197 | "lastModified": 1646506091, 198 | "narHash": "sha256-sWNAJE2m+HOh1jtXlHcnhxsj6/sXrHgbqVNcVRlveK4=", 199 | "owner": "NixOS", 200 | "repo": "nixpkgs", 201 | "rev": "3e644bd62489b516292c816f70bf0052c693b3c7", 202 | "type": "github" 203 | }, 204 | "original": { 205 | "owner": "NixOS", 206 | "ref": "nixpkgs-unstable", 207 | "repo": "nixpkgs", 208 | "type": "github" 209 | } 210 | }, 211 | "purescript-language-server": { 212 | "flake": false, 213 | "locked": { 214 | "lastModified": 1623009397, 215 | "narHash": "sha256-7gHA3ny5NmABAaEByUeyCji8j2knw+AAPw+PCnPkyBE=", 216 | "owner": "ursi", 217 | "repo": "purescript-language-server", 218 | "rev": "8a6c20402bab013c3510ab262aebc151059c2a2e", 219 | "type": "github" 220 | }, 221 | "original": { 222 | "owner": "ursi", 223 | "ref": "purs-nix", 224 | "repo": "purescript-language-server", 225 | "type": "github" 226 | } 227 | }, 228 | "purs-nix": { 229 | "inputs": { 230 | "builders": "builders", 231 | "deadnix": "deadnix", 232 | "easy-ps": "easy-ps", 233 | "make-shell": "make-shell_2", 234 | "nixpkgs": "nixpkgs_4", 235 | "purescript-language-server": "purescript-language-server", 236 | "utils": "utils_2" 237 | }, 238 | "locked": { 239 | "lastModified": 1653168330, 240 | "narHash": "sha256-HjmmJcIbV0s2XABcbU/ObCTVqmWTOW9nhZ4bZP42Cxs=", 241 | "owner": "ursi", 242 | "repo": "purs-nix", 243 | "rev": "39aaa35deddbdd92e74027fd642cb0e685fd0338", 244 | "type": "github" 245 | }, 246 | "original": { 247 | "owner": "ursi", 248 | "repo": "purs-nix", 249 | "type": "github" 250 | } 251 | }, 252 | "root": { 253 | "inputs": { 254 | "make-shell": "make-shell", 255 | "nixpkgs": "nixpkgs", 256 | "purs-nix": "purs-nix", 257 | "utils": "utils_3" 258 | } 259 | }, 260 | "utils": { 261 | "locked": { 262 | "lastModified": 1638122382, 263 | "narHash": "sha256-sQzZzAbvKEqN9s0bzWuYmRaA03v40gaJ4+iL1LXjaeI=", 264 | "owner": "numtide", 265 | "repo": "flake-utils", 266 | "rev": "74f7e4319258e287b0f9cb95426c9853b282730b", 267 | "type": "github" 268 | }, 269 | "original": { 270 | "owner": "numtide", 271 | "repo": "flake-utils", 272 | "type": "github" 273 | } 274 | }, 275 | "utils_2": { 276 | "inputs": { 277 | "flake-utils": "flake-utils" 278 | }, 279 | "locked": { 280 | "lastModified": 1644029741, 281 | "narHash": "sha256-Kfip3zjNQ/40xJWH296MDEFKBBPnEPupjUvjdqhKNYg=", 282 | "owner": "ursi", 283 | "repo": "flake-utils", 284 | "rev": "6102802ae96c56af1d2d9fc297029f515f49e332", 285 | "type": "github" 286 | }, 287 | "original": { 288 | "owner": "ursi", 289 | "ref": "8", 290 | "repo": "flake-utils", 291 | "type": "github" 292 | } 293 | }, 294 | "utils_3": { 295 | "inputs": { 296 | "flake-utils": "flake-utils_2" 297 | }, 298 | "locked": { 299 | "lastModified": 1651458199, 300 | "narHash": "sha256-QDdW1ELhvXBw5ijByPLeDl9YKKIAPWjXTOfYsmB1f+c=", 301 | "owner": "ursi", 302 | "repo": "flake-utils", 303 | "rev": "c30f5538ce102474745b78d2806a4342e9b0e2cd", 304 | "type": "github" 305 | }, 306 | "original": { 307 | "owner": "ursi", 308 | "ref": "8", 309 | "repo": "flake-utils", 310 | "type": "github" 311 | } 312 | } 313 | }, 314 | "root": "root", 315 | "version": 7 316 | } 317 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { inputs = 2 | { make-shell.url = "github:ursi/nix-make-shell/1"; 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 4 | purs-nix.url ="github:ursi/purs-nix"; 5 | utils.url = "github:ursi/flake-utils/8"; 6 | }; 7 | 8 | outputs = { nixpkgs, utils, ... }@inputs: 9 | utils.apply-systems { inherit inputs; } 10 | ({ make-shell, pkgs, purs-nix, ... }: 11 | let 12 | inherit (purs-nix) ps-pkgs ps-pkgs-ns purs; 13 | package = import ./package.nix purs-nix; 14 | 15 | inherit 16 | (purs 17 | { inherit (package) dependencies; 18 | test-dependencies = [ ps-pkgs."assert" ps-pkgs-ns.ursi.prelude ]; 19 | srcs = [ ./src ]; 20 | } 21 | ) 22 | command; 23 | in 24 | { devShell = 25 | make-shell 26 | { packages = 27 | with pkgs; 28 | [ nodejs 29 | nodePackages.bower 30 | nodePackages.pulp 31 | purs-nix.purescript 32 | (command { inherit package; }) 33 | ]; 34 | }; 35 | } 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /package.nix: -------------------------------------------------------------------------------- 1 | { ps-pkgs, licenses, ... }: 2 | with ps-pkgs; 3 | { version = "0.2.3"; 4 | 5 | dependencies = 6 | [ foldable-traversable 7 | foreign-object 8 | maybe 9 | prelude 10 | return 11 | strings 12 | ]; 13 | 14 | pursuit = 15 | { name = "substitute"; 16 | repo = "https://github.com/ursi/purescript-substitute.git"; 17 | license = licenses.bsd3; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/Substitute.purs: -------------------------------------------------------------------------------- 1 | module Substitute 2 | ( module Type.Row.Homogeneous 3 | , normalize 4 | , makeSubstituter 5 | , Options 6 | , defaultOptions 7 | , substitute 8 | , minimalOptions 9 | ) where 10 | 11 | import Prelude 12 | 13 | import Data.Array as Array 14 | import Data.Array.NonEmpty (NonEmptyArray) 15 | import Data.Array.NonEmpty as ArrayNE 16 | import Data.Foldable (foldl) 17 | import Data.Maybe (Maybe(..), fromMaybe, maybe) 18 | import Data.String (Pattern(..)) 19 | import Data.String as String 20 | import Data.String.CodeUnits (toCharArray) 21 | import Data.String.CodeUnits as StringC 22 | import Data.Tuple (snd) 23 | import Data.Tuple.Nested ((/\)) 24 | import Foreign.Object as Obj 25 | import Return (Return(..)) 26 | import Return.Folds as Rfolds 27 | import Type.Row.Homogeneous (class Homogeneous) 28 | 29 | toLines :: String -> NonEmptyArray String 30 | toLines = 31 | String.split (Pattern "\n") 32 | >>> ArrayNE.fromArray 33 | >>> fromMaybe (pure "") 34 | 35 | -- | Remove whitespace in a way that lets you use multi-line strings independent of indentation, and allows the first line to be lined up with the rest of the string. Single-line strings are unchanged. 36 | -- |``` 37 | -- | -- str1 = str2 38 | -- | 39 | -- | str1 = 40 | -- | normalize 41 | -- | """ 42 | -- | foo 43 | -- | 44 | -- | bar 45 | -- | baz 46 | -- | """ 47 | -- | 48 | -- | str2 = 49 | -- | """foo 50 | -- | 51 | -- | bar 52 | -- | baz 53 | -- | """ 54 | -- |``` 55 | normalize :: String -> String 56 | normalize str = 57 | (if String.take 1 str == "\n" then 58 | String.drop 1 str 59 | else 60 | str 61 | ) 62 | # toLines 63 | # \lines -> 64 | let 65 | blankLineRemoved = 66 | ArrayNE.unsnoc lines 67 | # \{ init, last } -> 68 | if StringC.countPrefix (eq ' ') last == String.length last then 69 | if Array.null init then 70 | [ "" ] 71 | else 72 | init 73 | else 74 | ArrayNE.toArray lines 75 | 76 | minSpaces = 77 | foldl 78 | (\acc line -> 79 | if line == "" then 80 | acc 81 | else 82 | StringC.countPrefix (eq ' ') line 83 | # if acc == -1 then 84 | identity 85 | else 86 | min acc 87 | ) 88 | (-1) 89 | blankLineRemoved 90 | in 91 | foldl 92 | (\(acc /\ first) line -> 93 | (String.drop minSpaces line 94 | # if first then 95 | identity 96 | else \l -> acc <> "\n" <> l 97 | ) 98 | /\ false 99 | ) 100 | ("" /\ true) 101 | blankLineRemoved 102 | # \(s /\ _) -> 103 | if ArrayNE.length lines == Array.length blankLineRemoved then 104 | s 105 | else 106 | s <> "\n" 107 | 108 | data State 109 | = EnteringTemplate 110 | | GettingKey String 111 | | Continuing 112 | | Skipping 113 | 114 | type ParseState 115 | = { leadingSpaces :: Int 116 | , state :: State 117 | } 118 | 119 | -- | ### marker, open, close :: Char 120 | -- | These three characters are used for detecting places to substitute values. Unless preceded by a `\`, sequences of the form `key` will be replaced by the value at `key` in the substitution record, if it exists. When it doesn't exist, see `missing` below. 121 | -- | 122 | -- | ### missing :: String -> String 123 | -- | When there is no value associated with `key`, the substituter returns the result of passing `key` into `missing`. 124 | -- | 125 | -- | ### normalizeString :: Boolean 126 | -- | Use `normalize` on the string passed to the function. 127 | -- | 128 | -- | ### normalizeSubstitutions :: Boolean 129 | -- | Use `normalize` on the substitutions. 130 | -- | 131 | -- | ### indent :: Boolean 132 | -- | When substituting in multi-line strings, pad with the appropriate whitespace so that all the lines are at the indentation level of the marker. Empty lines are not padded. 133 | -- | 134 | -- | 135 | -- | ``` 136 | -- | -- str1 = str2 137 | -- | 138 | -- | str1 = 139 | -- | substitute 140 | -- | """ 141 | -- | f = do 142 | -- | ${body} 143 | -- | """ 144 | -- | { body: 145 | -- | """ 146 | -- | log foo 147 | -- | log bar 148 | -- | log baz 149 | -- | """ 150 | -- | } 151 | -- | 152 | -- | str2 = 153 | -- | """f = do 154 | -- | log foo 155 | -- | log bar 156 | -- | log baz 157 | -- | """ 158 | -- | ``` 159 | -- | 160 | -- | ### suppress :: Boolean 161 | -- | When substituting in multi-line strings that end in a `\n`, drop the `\n`. With `suppress = false`, `str1` in the example above evaluates to 162 | -- | ``` 163 | -- | """f = do 164 | -- | log foo 165 | -- | log bar 166 | -- | log baz 167 | -- | 168 | -- | """ 169 | -- | ``` 170 | type Options 171 | = { marker :: Char 172 | , open :: Char 173 | , close :: Char 174 | , missing :: String -> String 175 | , normalizeString :: Boolean 176 | , normalizeSubstitutions :: Boolean 177 | , indent :: Boolean 178 | , suppress :: Boolean 179 | } 180 | 181 | -- | ``` 182 | -- | { marker: '$' 183 | -- | , open: '{' 184 | -- | , close: '}' 185 | -- | , missing: \key -> "[MISSING KEY: \"" <> key <> "\"]" 186 | -- | , normalizeString: true 187 | -- | , normalizeSubstitutions: true 188 | -- | , indent: true 189 | -- | , suppress: true 190 | -- | } 191 | -- | ``` 192 | defaultOptions :: Options 193 | defaultOptions = 194 | { marker: '$' 195 | , open: '{' 196 | , close: '}' 197 | , missing: \key -> "[MISSING KEY: \"" <> key <> "\"]" 198 | , normalizeString: true 199 | , normalizeSubstitutions: true 200 | , indent: true 201 | , suppress: true 202 | } 203 | 204 | makeSubstituter :: 205 | ∀ r. 206 | Homogeneous r String => 207 | Options -> 208 | String -> 209 | Record r -> 210 | String 211 | makeSubstituter 212 | { marker 213 | , open 214 | , close 215 | , missing 216 | , normalizeString 217 | , normalizeSubstitutions 218 | , indent 219 | , suppress 220 | } 221 | templateStr 222 | subsRec = 223 | let 224 | subs = Obj.fromHomogeneous subsRec 225 | 226 | chars = 227 | toCharArray 228 | $ if normalizeString then 229 | normalize templateStr 230 | else 231 | templateStr 232 | in 233 | Rfolds.foldl 234 | (\(state'@{ leadingSpaces, state } /\ str) char -> 235 | let 236 | charS = StringC.singleton char 237 | plusOneState = state' { leadingSpaces = leadingSpaces + 1 } 238 | in 239 | case state, char of 240 | _, '\n' -> Cont $ state' { leadingSpaces = 0, state = Continuing } /\ (str <> charS) 241 | Continuing, '\\' -> Cont $ plusOneState { state = Skipping } /\ (str <> charS) 242 | Continuing, _ -> 243 | if char == marker then 244 | Cont $ state' { state = EnteringTemplate } /\ (str <> charS) 245 | else 246 | Cont $ plusOneState { state = Continuing } /\ (str <> charS) 247 | EnteringTemplate, _ -> 248 | if char == open then 249 | Cont $ state' { state = GettingKey "" } /\ StringC.dropRight 1 str 250 | else 251 | Cont $ plusOneState { state = Continuing } /\ (str <> charS) 252 | GettingKey key, _ -> 253 | if char == close then case Obj.lookup key subs of 254 | Just value -> 255 | Cont 256 | $ state' { state = Continuing } 257 | /\ (str 258 | <> ((if normalizeSubstitutions then 259 | normalize value 260 | else 261 | value 262 | ) 263 | # (\v -> 264 | unsnocString v 265 | # maybe v \{ init, last } -> 266 | if last == '\n' && suppress then 267 | init 268 | else 269 | v 270 | ) 271 | # \v -> 272 | if indent then 273 | toLines v 274 | # \lines -> case ArrayNE.uncons lines of 275 | { head, tail } -> 276 | head 277 | <> foldl 278 | (\acc line -> 279 | acc 280 | <> "\n" 281 | <> (if line == "" then 282 | "" 283 | else 284 | rep leadingSpaces " " 285 | ) 286 | <> line 287 | ) 288 | "" 289 | tail 290 | else 291 | v 292 | ) 293 | ) 294 | Nothing -> Return $ state' /\ missing key 295 | else 296 | Cont $ state' { state = GettingKey $ key <> charS } /\ str 297 | Skipping, '\\' -> Cont $ plusOneState { state = Skipping } /\ (str <> charS) 298 | Skipping, _ -> 299 | if char == marker then 300 | Cont $ state' { state = Continuing } /\ (StringC.dropRight 1 str <> charS) 301 | else 302 | Cont $ plusOneState { state = Continuing } /\ (str <> charS) 303 | ) 304 | ({ leadingSpaces: 0 305 | , state: Continuing 306 | } 307 | /\ "" 308 | ) 309 | chars 310 | # snd 311 | 312 | -- | `makeSubstituter defaultOptions` 313 | substitute :: ∀ r. Homogeneous r String => String -> Record r -> String 314 | substitute = makeSubstituter defaultOptions 315 | 316 | unsnocString :: String -> Maybe { init :: String, last :: Char } 317 | unsnocString s = 318 | let 319 | lengthm1 = String.length s - 1 320 | in 321 | StringC.charAt lengthm1 s 322 | <#> { last: _, init: String.take lengthm1 s } 323 | 324 | rep :: Int -> String -> String 325 | rep n s 326 | | n == 0 = "" 327 | | otherwise = s <> rep (n - 1) s 328 | 329 | -- | ``` 330 | -- | { marker: '$' 331 | -- | , open: '{' 332 | -- | , close: '}' 333 | -- | , missing: \key -> "[MISSING KEY: \"" <> key <> "\"]" 334 | -- | , normalizeString: false 335 | -- | , normalizeSubstitutions: false 336 | -- | , indent: false 337 | -- | , suppress: false 338 | -- | } 339 | -- | ``` 340 | minimalOptions :: Options 341 | minimalOptions = 342 | { marker: '$' 343 | , open: '{' 344 | , close: '}' 345 | , missing: \key -> "[MISSING KEY: \"" <> key <> "\"]" 346 | , normalizeString: false 347 | , normalizeSubstitutions: false 348 | , indent: false 349 | , suppress: false 350 | } 351 | -------------------------------------------------------------------------------- /test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import MasonPrelude 4 | import Substitute (defaultOptions, normalize, substitute, makeSubstituter) 5 | import Test.Assert (assert) 6 | 7 | main :: Effect Unit 8 | main = do 9 | test "1" 10 | ( normalize 11 | """ 12 | hi 13 | how 14 | are you 15 | """ 16 | ) 17 | """hi 18 | how 19 | are you 20 | """ 21 | test "2" 22 | ( normalize 23 | """ 24 | hi 25 | how 26 | are you""" 27 | ) 28 | """hi 29 | how 30 | are you""" 31 | test "3" 32 | ( normalize 33 | """ 34 | hi 35 | how 36 | are you 37 | """ 38 | ) 39 | """hi 40 | how 41 | are you 42 | """ 43 | test "4" 44 | ( normalize 45 | """ 46 | hi 47 | how 48 | are you 49 | """ 50 | ) 51 | """ hi 52 | how 53 | are you 54 | """ 55 | test "5" (substitute "hi ${name}, how are you" { name: "Mason" }) 56 | "hi Mason, how are you" 57 | test "6" 58 | ( substitute 59 | """ 60 | hi ${name}, 61 | how are you 62 | """ 63 | { name: "Mason" } 64 | ) 65 | """hi Mason, 66 | how are you 67 | """ 68 | test "6" 69 | ( substitute 70 | """ 71 | hi 72 | ${name}, 73 | how are you 74 | """ 75 | { name: "Mason" } 76 | ) 77 | """hi 78 | Mason, 79 | how are you 80 | """ 81 | test "8" 82 | ( substitute 83 | """ 84 | hi 85 | ${name}, 86 | how are you 87 | """ 88 | { name: "Mason" } 89 | ) 90 | """hi 91 | Mason, 92 | how are you 93 | """ 94 | test "9" 95 | (substitute "hi \\${name}, how are you" { name: "Mason" }) 96 | "hi ${name}, how are you" 97 | test "10" (substitute "$" {}) "$" 98 | test "11" (substitute "${name}" { name: "Mason" }) "Mason" 99 | test "12" (substitute "${name}" {}) "[MISSING KEY: \"name\"]" 100 | test "13" 101 | ( makeSubstituter 102 | (defaultOptions { missing = const "missing" }) 103 | "foo ${name} bar" 104 | {} 105 | ) 106 | "missing" 107 | test "14" 108 | ( substitute 109 | """ 110 | hi 111 | ${test} 112 | are you 113 | """ 114 | { test: 115 | """ 116 | Mason, 117 | how 118 | """ 119 | } 120 | ) 121 | """hi 122 | Mason, 123 | how 124 | are you 125 | """ 126 | test "15" 127 | ( makeSubstituter (defaultOptions { suppress = false }) 128 | """ 129 | hi 130 | ${test} 131 | are you 132 | """ 133 | { test: 134 | """ 135 | Mason, 136 | how 137 | """ 138 | } 139 | ) 140 | """hi 141 | Mason, 142 | how 143 | 144 | are you 145 | """ 146 | test "16" 147 | ( makeSubstituter 148 | ( defaultOptions 149 | { indent = false 150 | , suppress = false 151 | } 152 | ) 153 | """ 154 | hi 155 | ${test} 156 | are you 157 | """ 158 | { test: 159 | """ 160 | Mason, 161 | how 162 | """ 163 | } 164 | ) 165 | """hi 166 | Mason, 167 | how 168 | 169 | are you 170 | """ 171 | test "17" 172 | ( makeSubstituter (defaultOptions { indent = false }) 173 | """ 174 | hi 175 | ${test} 176 | are you 177 | """ 178 | { test: 179 | """ 180 | Mason, 181 | how 182 | """ 183 | } 184 | ) 185 | """hi 186 | Mason, 187 | how 188 | are you 189 | """ 190 | test "18" (normalize "") "" 191 | test "19" 192 | ( normalize 193 | """ 194 | """ 195 | ) 196 | "" 197 | test "20" 198 | ( normalize 199 | """ 200 | 201 | """ 202 | ) 203 | "\n" 204 | test "21" (normalize "hi how are you") "hi how are you" 205 | test "22" 206 | ( normalize 207 | """ 208 | 209 | """ 210 | ) 211 | "\n" 212 | test "22" 213 | ( normalize 214 | """ 215 | 216 | test 217 | 218 | """ 219 | ) 220 | """ 221 | test 222 | 223 | """ 224 | test "23" 225 | ( normalize 226 | """ 227 | hi 228 | 229 | how 230 | 231 | are you 232 | """ 233 | ) 234 | """hi 235 | 236 | how 237 | 238 | are you 239 | """ 240 | test "24" 241 | ( makeSubstituter 242 | ( defaultOptions 243 | { indent = false 244 | , suppress = false 245 | , normalizeString = false 246 | , normalizeSubstitutions = false 247 | } 248 | ) 249 | """ 250 | hi 251 | ${test} 252 | are you 253 | """ 254 | { test: 255 | """ 256 | Mason, 257 | how 258 | """ 259 | } 260 | ) 261 | """ 262 | hi 263 | 264 | Mason, 265 | how 266 | 267 | are you 268 | """ 269 | test "25" 270 | ( substitute 271 | """ 272 | hi 273 | ${test} 274 | are you 275 | """ 276 | { test: 277 | """ 278 | Mason, 279 | 280 | how 281 | """ 282 | } 283 | ) 284 | """hi 285 | Mason, 286 | 287 | how 288 | are you 289 | """ 290 | test "26" (substitute """\\${test}""" {}) """\${test}""" 291 | test "27" (substitute """\\\${test}""" {}) """\\${test}""" 292 | test "28" (substitute """\a\\b\\\c""" {}) """\a\\b\\\c""" 293 | test "29" 294 | ( normalize 295 | """foo 296 | bar 297 | """ 298 | ) 299 | """foo 300 | bar 301 | """ 302 | test "30" 303 | (substitute 304 | """ 305 | { ${test} 306 | } 307 | """ 308 | { test: 309 | """ 310 | foo 311 | bar 312 | baz 313 | """ 314 | } 315 | ) 316 | """{ foo 317 | bar 318 | baz 319 | } 320 | """ 321 | -- TODO: what if 'test2' is a multi-line string? 322 | test "31" 323 | (substitute 324 | """ 325 | { ${test} ${test2} 326 | } 327 | """ 328 | { test: 329 | """ 330 | foo 331 | bar 332 | baz 333 | """ 334 | , test2: "qux" 335 | } 336 | ) 337 | """{ foo 338 | bar 339 | baz qux 340 | } 341 | """ 342 | test "docs: normalize" 343 | ( normalize 344 | """ 345 | foo 346 | 347 | bar 348 | baz 349 | """ 350 | ) 351 | """foo 352 | 353 | bar 354 | baz 355 | """ 356 | test "docs: indent" 357 | ( substitute 358 | """ 359 | f = do 360 | ${body} 361 | """ 362 | { body: 363 | """ 364 | log foo 365 | log bar 366 | log baz 367 | """ 368 | } 369 | ) 370 | """f = do 371 | log foo 372 | log bar 373 | log baz 374 | """ 375 | test "docs: suppress" 376 | ( makeSubstituter (defaultOptions { suppress = false }) 377 | """ 378 | f = do 379 | ${body} 380 | """ 381 | { body: 382 | """ 383 | log foo 384 | log bar 385 | log baz 386 | """ 387 | } 388 | ) 389 | """f = do 390 | log foo 391 | log bar 392 | log baz 393 | 394 | """ 395 | test "docs: readme" 396 | ( substitute 397 | """ 398 | ${name} :: Int -> Int -> ${type} 399 | ${name} a b = 400 | let 401 | ${lets} 402 | in 403 | c - d 404 | """ 405 | { name: "myFunction" 406 | , "type": "Int" 407 | , lets: 408 | """ 409 | c = a + b 410 | 411 | d = a * b 412 | """ 413 | } 414 | ) 415 | """myFunction :: Int -> Int -> Int 416 | myFunction a b = 417 | let 418 | c = a + b 419 | 420 | d = a * b 421 | in 422 | c - d 423 | """ 424 | 425 | test :: String -> String -> String -> Effect Unit 426 | test label s1 s2 = 427 | if s1 == s2 then 428 | pure unit 429 | else do 430 | log $ "failure: " <> label 431 | logShow $ "got:" <> s1 432 | logShow $ "exp:" <> s2 433 | assert false 434 | --------------------------------------------------------------------------------