├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── src └── Data │ └── String │ └── VerEx.purs └── test └── Main.purs /.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /.psci* 6 | /src/.webpack.js 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 David Peter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # purescript-verbal-expressions 2 | 3 | A free monad implementation of [Verbal Expressions](https://github.com/VerbalExpressions/JSVerbalExpressions) for [PureScript](https://github.com/purescript/purescript). 4 | 5 | * [Module documentation](http://pursuit.purescript.org/packages/purescript-verbal-expressions/) 6 | * [Test suite](https://github.com/VerbalExpressions/purescript-verbal-expressions/blob/master/test/Main.purs) 7 | 8 | ## Examples 9 | 10 | ### Basic testing 11 | We can use do-notation to construct verbal expressions: 12 | ``` purs 13 | number :: VerEx 14 | number = do 15 | startOfLine 16 | possibly (anyOf "+-") 17 | some digit 18 | possibly do 19 | find "." 20 | some digit 21 | endOfLine 22 | 23 | > test number "42" 24 | true 25 | 26 | > test number "+3.14" 27 | true 28 | 29 | > test number "3." 30 | false 31 | ``` 32 | The monadic interface allows us to bind the indices of capture groups to named expressions: 33 | ``` purs 34 | pattern :: VerEx 35 | pattern = do 36 | firstWord <- capture word 37 | whitespace 38 | word 39 | whitespace 40 | findAgain firstWord 41 | ``` 42 | This pattern matches "foo bar *foo*" but not "foo bar *baz*". 43 | 44 | ### Matching 45 | Here, we use the result of the monadic action to return an array of capture groups. Note that optional capturing groups are possible: 46 | ``` purs 47 | number = do 48 | startOfLine 49 | intPart <- capture (some digit) 50 | floatPart <- possibly do 51 | find "." 52 | capture (some digit) 53 | endOfLine 54 | pure [intPart, floatPart] 55 | 56 | > match number "3.14" 57 | Just [Just "3", Just "14"] 58 | 59 | > match number "42" 60 | Just [Just "42", Nothing] 61 | 62 | > match number "." 63 | Nothing 64 | ``` 65 | 66 | ### Replacing 67 | If, instead, we return a string from the monadic action, we can use it as a replacement string (with 'named' capture groups): 68 | ``` purs 69 | swapWords :: String -> String 70 | swapWords = replace do 71 | first <- capture word 72 | blank <- capture (some whitespace) 73 | second <- capture word 74 | 75 | replaceWith (insert second <> insert blank <> insert first) 76 | 77 | > swapWords "Foo Bar" 78 | "Bar Foo" 79 | ``` 80 | Note that `replaceWith` is just an alias for `pure`. 81 | 82 | For more examples, see the [tests](test/Main.purs). 83 | 84 | ## Installation 85 | ``` 86 | bower install purescript-verbal-expressions 87 | ``` 88 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-verbal-expressions", 3 | "moduleType": [ 4 | "node" 5 | ], 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/VerbalExpressions/purescript-verbal-expressions.git" 9 | }, 10 | "license": "MIT", 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "output" 16 | ], 17 | "dependencies": { 18 | "purescript-strings": "^4.0.0", 19 | "purescript-free": "^5.1.0", 20 | "purescript-transformers": "^4.1.0", 21 | "purescript-tuples": "^5.1.0" 22 | }, 23 | "devDependencies": { 24 | "purescript-test-unit": "^14.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Data/String/VerEx.purs: -------------------------------------------------------------------------------- 1 | -- | This module contains a free monad implementation of 2 | -- | [Verbal Expressions](https://github.com/VerbalExpressions/JSVerbalExpressions). 3 | -- | for PureScript. 4 | module Data.String.VerEx 5 | ( VerExF() 6 | , VerExM() 7 | , VerEx() 8 | , VerExReplace() 9 | , VerExMatch() 10 | , CaptureGroup() 11 | -- VerEx combinators 12 | , startOfLine' 13 | , startOfLine 14 | , endOfLine' 15 | , endOfLine 16 | , find 17 | , possibly 18 | , anything 19 | , anythingBut 20 | , something 21 | , anyOf 22 | , some 23 | , many 24 | , exactly 25 | , lineBreak 26 | , tab 27 | , word 28 | , digit 29 | , upper 30 | , lower 31 | , whitespace 32 | , withAnyCase 33 | -- Capture groups 34 | , capture 35 | , findAgain 36 | -- Replacements 37 | , replaceWith 38 | , insert 39 | -- Conversion to Regex 40 | , toRegex 41 | -- Pattern matching 42 | , test 43 | , replace 44 | , match 45 | ) where 46 | 47 | import Prelude hiding (add) 48 | 49 | import Control.Monad.Free (Free, liftF, foldFree) 50 | import Control.Monad.State (State, modify, get, put, runState) 51 | import Data.Array (index) 52 | import Data.Array as Array 53 | import Data.Either (fromRight) 54 | import Data.Maybe (Maybe) 55 | import Data.String.Regex (Regex, match, replace, test, source, parseFlags, regex) as R 56 | import Data.String.Regex.Flags (RegexFlags) as R 57 | import Data.Tuple (fst, snd) 58 | import Partial.Unsafe (unsafePartial) 59 | 60 | newtype CaptureGroup = CaptureGroup Int 61 | 62 | -- | The grammar for Verbal Expressions, used internally. 63 | data VerExF a 64 | = Add String a 65 | | StartOfLine Boolean a 66 | | EndOfLine Boolean a 67 | | AddFlags String a 68 | | AddSubexpression (VerExM a) (a -> a) 69 | | Capture VerEx (CaptureGroup -> a) 70 | 71 | -- | The free monad over the `VerExF` type constructor. 72 | type VerExM = Free VerExF 73 | 74 | -- | A monadic action that constructs a Verbal Expression. 75 | type VerEx = VerExM Unit 76 | 77 | -- | A monadic action that constructs a Verbal Expression and returns a 78 | -- | replacement string. 79 | type VerExReplace = VerExM String 80 | 81 | -- | A monadic action that constructs a Verbal Expression and returns an 82 | -- | array of capture group indices. 83 | type VerExMatch = VerExM (Array CaptureGroup) 84 | 85 | -- | Set whether or not the expression has to start at the beginning of the 86 | -- | line. Default: `false`. 87 | startOfLine' :: Boolean -> VerExM Unit 88 | startOfLine' flag = liftF $ StartOfLine flag unit 89 | 90 | -- | Mark the expression to start at the beginning of the line. 91 | startOfLine :: VerExM Unit 92 | startOfLine = startOfLine' true 93 | 94 | -- | Set whether or not the expression has to end at the end of the line. 95 | -- | Default: `false`. 96 | endOfLine' :: Boolean -> VerExM Unit 97 | endOfLine' flag = liftF $ EndOfLine flag unit 98 | 99 | -- | Mark the expression to end at the end of the line. 100 | endOfLine :: VerExM Unit 101 | endOfLine = endOfLine' true 102 | 103 | -- | Append additional Regex flags, used internally. 104 | addFlags :: String -> VerExM Unit 105 | addFlags flags = liftF $ AddFlags flags unit 106 | 107 | -- | Append a sub-expression in a non-capturing group 108 | addSubexpression :: forall a. VerExM a -> VerExM a 109 | addSubexpression inner = liftF $ AddSubexpression inner identity 110 | 111 | -- | Compile a regex 112 | regex :: String -> R.RegexFlags -> R.Regex 113 | regex pattern flags = unsafePartial $ fromRight $ R.regex pattern flags 114 | 115 | -- | Escape special regex characters 116 | escape :: String -> String 117 | escape = R.replace (regex "([\\].|*?+(){}^$\\\\:=[])" g) "\\$&" 118 | where g = R.parseFlags "g" 119 | 120 | -- | Internal function to add a pattern to the VerEx. 121 | add :: String -> VerExM Unit 122 | add str = liftF $ Add str unit 123 | 124 | -- | Add a string to the expression. 125 | find :: String -> VerExM Unit 126 | find str = add $ "(?:" <> escape str <> ")" 127 | 128 | -- | Add a sub-expression which might appear zero or one times. 129 | possibly :: forall a. VerExM a -> VerExM a 130 | possibly sub = addSubexpression sub <* add "?" 131 | 132 | -- | Match any charcter, any number of times. 133 | anything :: VerExM Unit 134 | anything = add "(?:.*)" 135 | 136 | -- | Match anything but the specified characters. 137 | anythingBut :: String -> VerExM Unit 138 | anythingBut str = add $ "(?:[^" <> escape str <> "]*)" 139 | 140 | -- | Match any charcter, at least one time. 141 | something :: VerExM Unit 142 | something = add "(?:.+)" 143 | 144 | -- | Any of the given characters. 145 | anyOf :: String -> VerExM Unit 146 | anyOf str = add $ "(?:[" <> escape str <> "])" 147 | 148 | -- | Repeat the inner expression one or more times. 149 | some :: VerEx -> VerExM Unit 150 | some pattern = addSubexpression pattern *> add "+" 151 | 152 | -- | Repeat the inner expression zero or more times. 153 | many :: VerEx -> VerExM Unit 154 | many pattern = addSubexpression pattern *> add "*" 155 | 156 | -- | Repeat the inner expression exactly the given number of times. 157 | exactly :: Int -> VerEx -> VerExM Unit 158 | exactly n pattern = addSubexpression pattern *> add ("{" <> show n <> "}") 159 | 160 | -- | Add universal line break expression. 161 | lineBreak :: VerExM Unit 162 | lineBreak = add "(?:(?:\\n)|(?:\\r\\n))" 163 | 164 | -- | Add expression to match a tab character. 165 | tab :: VerExM Unit 166 | tab = add "(?:\\t)" 167 | 168 | -- | Adds an expression to match a word. 169 | word :: VerExM Unit 170 | word = add "(?:\\w+)" 171 | 172 | -- | Adds an expression to match a single digit. 173 | digit :: VerExM Unit 174 | digit = add "\\d" 175 | 176 | -- | Adds an expression to match a single uppercase character (ASCII range). 177 | -- | Note that this will match uppercase and lowercase characters if 178 | -- | `withAnyCase` is used. 179 | upper :: VerExM Unit 180 | upper = add "[A-Z]" 181 | 182 | -- | Adds an expression to match a single lowercase character (ASCII range). 183 | -- | Note that this will match uppercase and lowercase characters if 184 | -- | `withAnyCase` is used. 185 | lower :: VerExM Unit 186 | lower = add "[a-z]" 187 | 188 | -- | Any whitespace character 189 | whitespace :: VerExM Unit 190 | whitespace = add "\\s" 191 | 192 | -- | Enable case-insensitive matching 193 | withAnyCase :: VerExM Unit 194 | withAnyCase = addFlags "i" 195 | 196 | -- | Add a new capture group which matches the given VerEx. Returns the index 197 | -- | of the capture group. 198 | capture :: VerEx -> VerExM CaptureGroup 199 | capture inner = liftF $ Capture inner identity 200 | 201 | -- | Match a previous capture group again (back reference). 202 | findAgain :: CaptureGroup -> VerExM Unit 203 | findAgain (CaptureGroup ind) = add $ "(?:\\" <> show ind <> ")" 204 | 205 | -- | Replace the matched string with the given replacement. 206 | replaceWith :: String -> VerExReplace 207 | replaceWith = pure 208 | 209 | -- | Add the contents of a given capture group in the replacement string. 210 | insert :: CaptureGroup -> String 211 | insert (CaptureGroup ind) = "$" <> show ind 212 | 213 | type VerExState = 214 | { startOfLine :: Boolean 215 | , endOfLine :: Boolean 216 | , flags :: String 217 | , pattern :: String 218 | , captureGroupIndex :: Int } 219 | 220 | empty :: VerExState 221 | empty = 222 | { startOfLine: false 223 | , endOfLine: false 224 | , flags: "" 225 | , pattern: "" 226 | , captureGroupIndex: 1 } 227 | 228 | -- | Natural transformation from `VerExF` to `State VerExState`. 229 | toVerExState :: forall a. VerExF a -> State VerExState a 230 | toVerExState (Add str a) = a <$ 231 | modify (\s -> s { pattern = s.pattern <> str }) 232 | toVerExState (StartOfLine flag a) = a <$ 233 | modify (\s -> s { startOfLine = flag }) 234 | toVerExState (EndOfLine flag a) = a <$ 235 | modify (\s -> s { endOfLine = flag }) 236 | toVerExState (AddFlags flags a) = a <$ 237 | modify (\s -> s { flags = s.flags <> flags }) 238 | toVerExState (AddSubexpression inner f) = f <$> do 239 | s <- get 240 | let res = toRegex' s.captureGroupIndex inner 241 | put s { pattern = s.pattern <> "(?:" <> R.source (res.regex) <> ")" 242 | , captureGroupIndex = res.lastIndex } 243 | pure res.result 244 | toVerExState (Capture inner f) = f <$> do 245 | s <- get 246 | let cg = s.captureGroupIndex 247 | res = toRegex' (cg + 1) inner 248 | put s { pattern = s.pattern <> "(" <> R.source (res.regex) <> ")" 249 | , captureGroupIndex = res.lastIndex } 250 | pure (CaptureGroup cg) 251 | 252 | -- | Convert a Verbal Expression to a Regular Expression. Also returns the 253 | -- | result of the monadic action and the last capture group index. The first 254 | -- | argument is the first capture group index that should be used. 255 | toRegex' :: forall a. Int -> VerExM a -> { result :: a, regex :: R.Regex, lastIndex :: Int } 256 | toRegex' first verex = { result, regex: regex', lastIndex } 257 | where 258 | both = runState (foldFree toVerExState verex) (empty { captureGroupIndex = first }) 259 | result = fst both 260 | verexS = snd both 261 | 262 | flags = R.parseFlags verexS.flags 263 | 264 | prefix = if verexS.startOfLine then "^" else "" 265 | suffix = if verexS.endOfLine then "$" else "" 266 | 267 | regex' = regex (prefix <> verexS.pattern <> suffix) flags 268 | lastIndex = verexS.captureGroupIndex 269 | 270 | -- | Convert a Verbal Expression to a Regular Expression. 271 | toRegex :: forall a. VerExM a -> R.Regex 272 | toRegex verex = _.regex (toRegex' 1 (void verex)) 273 | 274 | -- | Convert the pattern (without the flags) of a VerEx to a `String`. 275 | toString :: VerEx -> String 276 | toString verex = R.source (toRegex verex) 277 | 278 | -- | Check whether a given `String` matches the Verbal Expression. 279 | test :: forall a. VerExM a -> String -> Boolean 280 | test verex = R.test (toRegex verex) 281 | 282 | -- | Replace occurences of the `VerEx` with the `String` that is returned by 283 | -- | the monadic action. 284 | replace :: VerExReplace -> String -> String 285 | replace verex = R.replace pattern.regex pattern.result 286 | where pattern = toRegex' 1 (verex <* addFlags "g") 287 | 288 | -- | Match the `VerEx` against the string argument and (maybe) return an Array 289 | -- | of possible results from the specified capture groups. 290 | match :: VerExMatch -> String -> Maybe (Array (Maybe String)) 291 | match verex str = do 292 | matches <- Array.fromFoldable <$> maybeMatches 293 | pure (fromIndex matches <$> pattern.result) 294 | where pattern = toRegex' 1 verex 295 | maybeMatches = R.match pattern.regex str 296 | fromIndex matches (CaptureGroup j) = do 297 | maybeResult <- matches `index` j 298 | result <- maybeResult 299 | pure result 300 | -------------------------------------------------------------------------------- /test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | import Data.Maybe (Maybe(..)) 5 | 6 | import Effect (Effect) 7 | 8 | import Test.Unit as Unit 9 | import Test.Unit.Main (runTest) 10 | import Test.Unit.Assert (assert, assertFalse, equal) 11 | 12 | import Data.String.VerEx (VerEx, VerExMatch, digit, upper, lower, capture, 13 | find, match, endOfLine, some, possibly, startOfLine, 14 | exactly, replaceWith, anythingBut, replace, insert, 15 | word, whitespace, test, findAgain, withAnyCase, tab, 16 | lineBreak, many, anyOf, something, anything) 17 | 18 | url :: VerExMatch 19 | url = do 20 | startOfLine 21 | protocol <- capture do 22 | find "http" 23 | possibly $ find "s" 24 | find "://" 25 | domain <- capture do 26 | possibly $ find "www." 27 | anythingBut " " 28 | endOfLine 29 | 30 | pure [protocol, domain] 31 | 32 | number :: VerEx 33 | number = do 34 | startOfLine 35 | possibly (anyOf "+-") 36 | some digit 37 | possibly do 38 | find "." 39 | some digit 40 | endOfLine 41 | 42 | main :: Effect Unit 43 | main = runTest do 44 | Unit.test "URL VerEx" do 45 | let isUrl = test url 46 | assert "should match valid URL" $ isUrl "https://www.google.com" 47 | assert "should match valid URL" $ isUrl "http://google.com" 48 | assert "should match valid URL" $ isUrl "http://google.com" 49 | assertFalse "should not match invalid URL" $ isUrl "http://google com" 50 | assertFalse "should not match invalid URL" $ isUrl "ftp://google com" 51 | 52 | Unit.test "startOfLine" do 53 | let vStartOfLine = startOfLine *> find "a" 54 | assert "should match 'a' at start of the line" $ 55 | test vStartOfLine "a" 56 | assertFalse "should not match if no 'a' is at the start of the line" $ 57 | test vStartOfLine "ba" 58 | 59 | Unit.test "endOfLine" do 60 | let vEndOfLine = find "a" *> endOfLine 61 | assert "should match 'a' at the end of the line" $ 62 | test vEndOfLine "a" 63 | assertFalse "should not match if no 'a' is at the end of the line" $ 64 | test vEndOfLine "ab" 65 | 66 | Unit.test "find" do 67 | assert "should match a and then b" $ 68 | test (find "a" *> find "b") "ab" 69 | assert "should properly find special characters" $ 70 | test (find "^)(.$[") "^)(.$[" 71 | 72 | Unit.test "possibly" do 73 | let vPossibly = do 74 | find "a" 75 | possibly do 76 | find "(" 77 | some (find "bc") 78 | find ")" 79 | find "d" 80 | assert "should match" $ test vPossibly "ad" 81 | assert "should match" $ test vPossibly "a(bc)d" 82 | assert "should match" $ test vPossibly "a(bcbcbcbc)d" 83 | assertFalse "should not match" $ test vPossibly "a()d" 84 | assertFalse "should not match" $ test vPossibly "abcd" 85 | 86 | Unit.test "anything" do 87 | assert "should match any character" $ test anything "$(#!" 88 | assert "should match empty string" $ test anything "" 89 | 90 | Unit.test "anythingBut" do 91 | let vAnythingBut = startOfLine *> anythingBut "a" *> endOfLine 92 | assert "should match anything but an 'a'" $ test vAnythingBut "b" 93 | assert "should match the empty string" $ test vAnythingBut "" 94 | assertFalse "should not match an 'a'" $ test vAnythingBut "a" 95 | 96 | Unit.test "something" do 97 | assert "should match any character" $ test something "$(#!" 98 | assertFalse "should not match the empty string" $ test something "" 99 | 100 | Unit.test "anyOf" do 101 | let vAnyOf = startOfLine *> find "a" *> anyOf "xyz" 102 | assert "should match an x" $ test vAnyOf "ax" 103 | assert "should match a y" $ test vAnyOf "az" 104 | assertFalse "should not match a b" $ test vAnyOf "ab" 105 | 106 | Unit.test "some" do 107 | let vSome = startOfLine *> some (anyOf ".[]") *> endOfLine 108 | assert "should match a single occurence" $ test vSome "." 109 | assert "should handle special characters" $ test vSome "[" 110 | assert "should match more than one occurence" $ test vSome "[..]..]" 111 | assertFalse "should not match the 'a'" $ test vSome "..a.." 112 | assertFalse "should not match the empty string" $ test vSome "" 113 | 114 | Unit.test "many" do 115 | let vMany = startOfLine *> many whitespace *> endOfLine 116 | assert "should match a single occurence" $ test vMany " " 117 | assert "should match the empty string" $ test vMany "" 118 | assert "should match many occurences" $ test vMany " " 119 | assert "should handle the sub-expression correctly" $ test vMany " \t \t" 120 | 121 | Unit.test "lineBreak" do 122 | let vLineBreak = startOfLine *> find "abc" *> lineBreak *> find "def" 123 | assert "should match unix newlines" $ test vLineBreak "abc\ndef" 124 | assert "should match windows newlines" $ test vLineBreak "abc\r\ndef" 125 | assertFalse "should not match other things after the newline" $ 126 | test vLineBreak "abc\nghi" 127 | 128 | Unit.test "tab" do 129 | assert "should match a tab character" $ 130 | test (find "a" *> tab *> find "b") "a\tb" 131 | 132 | Unit.test "word" do 133 | assert "should match a whole word" $ 134 | test (word *> whitespace *> word) "Hello World" 135 | 136 | Unit.test "digit" do 137 | assert "should match any digit" $ 138 | test (find "(" *> some digit *> find ")") "(0123456789)" 139 | 140 | Unit.test "upper" do 141 | assert "should match uppercase ASCII characters" $ 142 | test (find "(" *> some upper *> find ")") "(ABCDEFGHIJKLMNOPQRSTUVWXYZ)" 143 | assertFalse "should not match anything else" $ 144 | test upper "42!#a" 145 | 146 | Unit.test "lower" do 147 | assert "should match lowercase ASCII characters" $ 148 | test (find "(" *> some lower *> find ")") "(abcdefghijklmnopqrstuvwxyz)" 149 | assertFalse "should not match anything else" $ 150 | test lower "42!#A" 151 | 152 | Unit.test "number VerEx" do 153 | let isNumber = test number 154 | assert "should match a single digit" $ isNumber "1" 155 | assert "should match an integer" $ isNumber "4242" 156 | assert "should match a signed integer" $ isNumber "+42" 157 | assert "should match a signed integer" $ isNumber "-42" 158 | assert "should match a float" $ isNumber "42.123" 159 | assert "should match a negative float" $ isNumber "-42.123" 160 | assertFalse "should not match a charater" $ isNumber "a" 161 | assertFalse "should not match just the float part" $ isNumber ".123" 162 | assertFalse "should not match a trailing '.'" $ isNumber "0." 163 | 164 | Unit.test "whitespace" do 165 | assert "should match all whitespace characters" $ 166 | test (find "a" *> some whitespace *> find "b") "a \n \t b" 167 | 168 | Unit.test "withAnyCase" do 169 | assertFalse "should be case-sensitive by default" $ 170 | test (find "foo") "Foo" 171 | assert "should enable case-insensitivity" $ 172 | test (withAnyCase *> find "foo") "Foo" 173 | 174 | Unit.test "capture" do 175 | let vCapture = do 176 | firstWord <- capture word 177 | whitespace 178 | word 179 | whitespace 180 | findAgain firstWord 181 | assert "should match 'foo bar foo'" $ 182 | test vCapture "foo bar foo" 183 | assertFalse "should not match 'foo bar baz'" $ 184 | test vCapture "foo bar baz" 185 | 186 | Unit.test "replace" do 187 | let verexReplace = do 188 | first <- capture word 189 | blank <- capture (some whitespace) 190 | second <- capture word 191 | replaceWith (insert second <> insert blank <> insert first) 192 | equal (replace verexReplace "Foo Bar") 193 | "Bar Foo" 194 | 195 | let censor = replace $ find "[" *> anythingBut "]" *> find "]" *> replaceWith "---" 196 | equal 197 | (censor "Censor [all!!] things [inside(42)] brackets") 198 | "Censor --- things --- brackets" 199 | 200 | Unit.test "match" do 201 | equal (match url "https://google.com") 202 | (Just [Just "https", Just "google.com"]) 203 | equal (match url "ftp://google.com") 204 | Nothing 205 | 206 | let date = do 207 | startOfLine 208 | year <- capture do 209 | possibly (exactly 2 digit) 210 | exactly 2 digit 211 | find "-" 212 | month <- capture (exactly 2 digit) 213 | find "-" 214 | day <- capture (exactly 2 digit) 215 | endOfLine 216 | pure [year, month, day] 217 | 218 | equal (match date "2016-01-11") 219 | (Just [Just "2016", Just "01", Just "11"]) 220 | equal (match date "16-01-11") 221 | (Just [Just "16", Just "01", Just "11"]) 222 | equal (match date "016-01-11") 223 | Nothing 224 | 225 | let matchNumber = match do 226 | startOfLine 227 | intPart <- capture (some digit) 228 | floatPart <- possibly do 229 | find "." 230 | capture (some digit) 231 | endOfLine 232 | 233 | pure [intPart, floatPart] 234 | 235 | equal (matchNumber "3.14") 236 | (Just [Just "3", Just "14"]) 237 | equal (matchNumber "42") 238 | (Just [Just "42", Nothing]) 239 | equal (matchNumber ".3") 240 | Nothing 241 | 242 | let matchNested = match do 243 | a <- capture digit 244 | find "," 245 | inner <- capture do 246 | void $ capture digit 247 | find "," 248 | b <- capture digit 249 | pure [a, inner, b] 250 | 251 | equal (matchNested "1,2,3") 252 | (Just [Just "1", Just "2", Just "3"]) 253 | --------------------------------------------------------------------------------