├── .github └── workflows │ └── test.yml ├── .gitignore ├── README.md ├── docs ├── gleam.js ├── highlightjs-gleam.js ├── index.css ├── index.html └── string │ └── parser │ └── index.html ├── gleam.toml ├── manifest.toml ├── src └── string │ └── parser.gleam └── test ├── currency.gleam ├── json.gleam └── string_parser_test.gleam /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | pull_request: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2.0.0 15 | - uses: erlef/setup-beam@v1.9.0 16 | with: 17 | otp-version: "23.2" 18 | gleam-version: "0.19.0" 19 | - run: gleam format --check src test 20 | - run: gleam deps download 21 | - run: gleam test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.o 3 | *.plt 4 | *.swo 5 | *.swp 6 | *~ 7 | *.beam 8 | *.ez 9 | build 10 | erl_crash.dump -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gleam-string-parser 2 | 3 | A simple parser combinator library built in Gleam. 4 | 5 | ## Quick start 6 | 7 | ```sh 8 | # Run the eunit tests 9 | rebar3 eunit 10 | 11 | # Run the Erlang REPL 12 | rebar3 shell 13 | ``` 14 | 15 | ## Installation 16 | 17 | If [available in Hex](https://rebar3.org/docs/configuration/dependencies/#declaring-dependencies) 18 | this package can be installed by adding `simple_parser` to your `rebar.config` dependencies: 19 | 20 | ```erlang 21 | {deps, [ 22 | string_parser 23 | ]}. 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/gleam.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | window.Gleam = function() { 4 | /* Global Object */ 5 | const self = {}; 6 | 7 | /* Public Properties */ 8 | 9 | self.hashOffset = undefined; 10 | 11 | /* Public Methods */ 12 | 13 | self.getProperty = function(property) { 14 | let value; 15 | try { 16 | value = localStorage.getItem(`Gleam.${property}`); 17 | } 18 | catch (_error) {} 19 | if (-1 < [null, undefined].indexOf(value)) { 20 | return gleamConfig[property].values[0].value; 21 | } 22 | return value; 23 | }; 24 | 25 | self.icons = function() { 26 | return Array.from(arguments).reduce( 27 | (acc, name) => 28 | `${acc} 29 | `, 30 | "" 31 | ); 32 | } 33 | 34 | self.scrollToHash = function() { 35 | const locationHash = arguments[0] || window.location.hash; 36 | const query = locationHash ? locationHash : "body"; 37 | const hashTop = document.querySelector(query).offsetTop; 38 | window.scrollTo(0, hashTop - self.hashOffset); 39 | return locationHash; 40 | }; 41 | 42 | self.toggleSidebar = function() { 43 | const previousState = 44 | bodyClasses.contains("drawer-open") ? "open" : "closed"; 45 | 46 | let state; 47 | if (0 < arguments.length) { 48 | state = false === arguments[0] ? "closed" : "open"; 49 | } 50 | else { 51 | state = "open" === previousState ? "closed" : "open"; 52 | } 53 | 54 | bodyClasses.remove(`drawer-${previousState}`); 55 | bodyClasses.add(`drawer-${state}`); 56 | 57 | if ("open" === state) { 58 | document.addEventListener("click", closeSidebar, false); 59 | } 60 | }; 61 | 62 | /* Private Properties */ 63 | 64 | const html = document.documentElement; 65 | const body = document.body; 66 | const bodyClasses = body.classList; 67 | const sidebar = document.querySelector(".sidebar"); 68 | const sidebarToggles = document.querySelectorAll(".sidebar-toggle"); 69 | const displayControls = document.createElement("div"); 70 | 71 | displayControls.classList.add("display-controls"); 72 | sidebar.appendChild(displayControls); 73 | 74 | /* Private Methods */ 75 | 76 | const initProperty = function(property) { 77 | const config = gleamConfig[property]; 78 | 79 | displayControls.insertAdjacentHTML( 80 | "beforeend", 81 | config.values.reduce( 82 | (acc, item, index) => { 83 | const tooltip = 84 | item.label 85 | ? `alt="${item.label}" title="${item.label}"` 86 | : ""; 87 | let inner; 88 | if (item.icons) { 89 | inner = self.icons(...item.icons); 90 | } 91 | else if (item.label) { 92 | inner = item.label; 93 | } 94 | else { 95 | inner = ""; 96 | } 97 | return ` 98 | ${acc} 99 | 100 | ${inner} 101 | 102 | `; 103 | }, 104 | ` 110 | ` 111 | ); 112 | 113 | setProperty(null, property, function() { 114 | return self.getProperty(property); 115 | }); 116 | }; 117 | 118 | const setProperty = function(_event, property) { 119 | const previousValue = self.getProperty(property); 120 | 121 | const update = 122 | 2 < arguments.length ? arguments[2] : gleamConfig[property].update; 123 | const value = update(); 124 | 125 | try { 126 | localStorage.setItem("Gleam." + property, value); 127 | } 128 | catch (_error) {} 129 | 130 | bodyClasses.remove(`${property}-${previousValue}`); 131 | bodyClasses.add(`${property}-${value}`); 132 | 133 | const isDefault = value === gleamConfig[property].values[0].value; 134 | const toggleClasses = 135 | document.querySelector(`#${property}-toggle`).classList; 136 | toggleClasses.remove(`toggle-${isDefault ? 1 : 0}`); 137 | toggleClasses.add(`toggle-${isDefault ? 0 : 1}`); 138 | 139 | try { 140 | gleamConfig[property].callback(value); 141 | } 142 | catch(_error) {} 143 | 144 | return value; 145 | } 146 | 147 | const setHashOffset = function() { 148 | const el = document.createElement("div"); 149 | el.style.cssText = 150 | ` 151 | height: var(--hash-offset); 152 | pointer-events: none; 153 | position: absolute; 154 | visibility: hidden; 155 | width: 0; 156 | `; 157 | body.appendChild(el); 158 | self.hashOffset = parseInt( 159 | getComputedStyle(el).getPropertyValue("height") || "0" 160 | ); 161 | body.removeChild(el); 162 | }; 163 | 164 | const closeSidebar = function(event) { 165 | if (! event.target.closest(".sidebar-toggle")) { 166 | document.removeEventListener("click", closeSidebar, false); 167 | self.toggleSidebar(false); 168 | } 169 | }; 170 | 171 | const init = function() { 172 | for (const property in gleamConfig) { 173 | initProperty(property); 174 | const toggle = document.querySelector(`#${property}-toggle`); 175 | toggle.addEventListener("click", function(event) { 176 | setProperty(event, property); 177 | }); 178 | } 179 | 180 | sidebarToggles.forEach(function(sidebarToggle) { 181 | sidebarToggle.addEventListener("click", function(event) { 182 | event.preventDefault(); 183 | self.toggleSidebar(); 184 | }); 185 | }); 186 | 187 | setHashOffset(); 188 | window.addEventListener("load", function(_event) { 189 | self.scrollToHash(); 190 | }); 191 | window.addEventListener("hashchange", function(_event) { 192 | self.scrollToHash(); 193 | }); 194 | 195 | document.querySelectorAll(` 196 | .module-name > a, 197 | .member-name a[href^='#'] 198 | `).forEach(function(title) { 199 | title.innerHTML = 200 | title.innerHTML.replace(/([A-Z])|([_/])/g, "$2$1"); 201 | }); 202 | }; 203 | 204 | /* Initialise */ 205 | 206 | init(); 207 | 208 | return self; 209 | }(); 210 | -------------------------------------------------------------------------------- /docs/highlightjs-gleam.js: -------------------------------------------------------------------------------- 1 | hljs.registerLanguage("gleam", function (hljs) { 2 | const KEYWORDS = 3 | "as assert case const external fn if import let " + 4 | "opaque pub todo try tuple type"; 5 | const STRING = { 6 | className: "string", 7 | variants: [{ begin: /"/, end: /"/ }], 8 | contains: [hljs.BACKSLASH_ESCAPE], 9 | relevance: 0, 10 | }; 11 | const NAME = { 12 | className: "variable", 13 | begin: "\\b[a-z][a-z0-9_]*\\b", 14 | relevance: 0, 15 | }; 16 | const DISCARD_NAME = { 17 | className: "comment", 18 | begin: "\\b_[a-z][a-z0-9_]*\\b", 19 | relevance: 0, 20 | }; 21 | const NUMBER = { 22 | className: "number", 23 | variants: [ 24 | { 25 | begin: "\\b0b([01_]+)", 26 | }, 27 | { 28 | begin: "\\b0o([0-7_]+)", 29 | }, 30 | { 31 | begin: "\\b0x([A-Fa-f0-9_]+)", 32 | }, 33 | { 34 | begin: "\\b(\\d[\\d_]*(\\.[0-9_]+)?([eE][+-]?[0-9_]+)?)", 35 | }, 36 | ], 37 | relevance: 0, 38 | }; 39 | 40 | return { 41 | name: "Gleam", 42 | aliases: ["gleam"], 43 | contains: [ 44 | hljs.C_LINE_COMMENT_MODE, 45 | STRING, 46 | { 47 | // bitstrings 48 | begin: "<<", 49 | end: ">>", 50 | contains: [ 51 | { 52 | className: "keyword", 53 | beginKeywords: 54 | "binary bytes int float bit_string bits utf8 utf16 utf32 " + 55 | "utf8_codepoint utf16_codepoint utf32_codepoint signed unsigned " + 56 | "big little native unit size", 57 | }, 58 | STRING, 59 | NUMBER, 60 | NAME, 61 | DISCARD_NAME, 62 | ], 63 | relevance: 10, 64 | }, 65 | { 66 | className: "function", 67 | beginKeywords: "fn", 68 | end: "\\(", 69 | excludeEnd: true, 70 | contains: [ 71 | { 72 | className: "title", 73 | begin: "[a-zA-Z0-9_]\\w*", 74 | relevance: 0, 75 | }, 76 | ], 77 | }, 78 | { 79 | className: "keyword", 80 | beginKeywords: KEYWORDS, 81 | }, 82 | { 83 | // Type names and constructors 84 | className: "title", 85 | begin: "\\b[A-Z][A-Za-z0-9_]*\\b", 86 | relevance: 0, 87 | }, 88 | { 89 | // float operators 90 | className: "operator", 91 | begin: "(\\+\\.|-\\.|\\*\\.|/\\.|<\\.|>\\.)", 92 | relevance: 10, 93 | }, 94 | { 95 | className: "operator", 96 | begin: "(->|\\|>|<<|>>|\\+|-|\\*|/|>=|<=|<|<|%|\\.\\.|\\|=|==|!=)", 97 | relevance: 0, 98 | }, 99 | NUMBER, 100 | NAME, 101 | DISCARD_NAME, 102 | ], 103 | }; 104 | }); 105 | document.querySelectorAll("pre code").forEach((block) => { 106 | if (block.className === "") { 107 | block.classList.add("gleam"); 108 | } 109 | hljs.highlightBlock(block); 110 | }); 111 | -------------------------------------------------------------------------------- /docs/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Karla:wght@400;700&family=Ubuntu+Mono&display=swap"); 2 | 3 | :root { 4 | /* Colours */ 5 | --black: #2a2020; 6 | --hard-black: #000; 7 | --pink: #ffaff3; 8 | --hot-pink: #d900b8; 9 | --white: #fff; 10 | --pink-white: #fff8fe; 11 | --mid-grey: #dfe2e5; 12 | --light-grey: #f5f5f5; 13 | --boi-blue: #a6f0fc; 14 | 15 | /* Derived colours */ 16 | --text: var(--black); 17 | --background: var(--white); 18 | --accented-background: var(--pink-white); 19 | --code-border: var(--pink); 20 | --code-background: var(--light-grey); 21 | --table-border: var(--mid-grey); 22 | --table-background: var(--pink-white); 23 | --links: var(--hot-pink); 24 | --accent: var(--pink); 25 | 26 | /* Sizes */ 27 | --content-width: 680px; 28 | --header-height: 60px; 29 | --hash-offset: calc(var(--header-height) * 1.67); 30 | --sidebar-width: 240px; 31 | --gap: 24px; 32 | --small-gap: calc(var(--gap) / 2); 33 | --tiny-gap: calc(var(--small-gap) / 2); 34 | --large-gap: calc(var(--gap) * 2); 35 | --sidebar-toggle-size: 33px; 36 | 37 | /* etc */ 38 | --shadow: 39 | 0 0 0 1px rgba(50, 50, 93, .075), 40 | 0 0 1px #e9ecef, 41 | 0 2px 4px -2px rgba(138, 141, 151, .6); 42 | --nav-shadow: 0 0 6px 2px rgba(0, 0, 0, .1); 43 | } 44 | 45 | * { 46 | box-sizing: border-box; 47 | } 48 | 49 | body, 50 | html { 51 | padding: 0; 52 | margin: 0; 53 | font-family: "Karla", sans-serif; 54 | font-size: 17px; 55 | line-height: 1.4; 56 | position: relative; 57 | min-height: 100vh; 58 | word-break: break-word; 59 | } 60 | 61 | html { 62 | /* This is necessary so hash targets appear below the fixed header */ 63 | scroll-padding-top: var(--hash-offset); 64 | } 65 | 66 | a, 67 | a:visited { 68 | color: var(--links); 69 | text-decoration: none; 70 | } 71 | 72 | a:hover { 73 | text-decoration: underline; 74 | } 75 | 76 | button, 77 | select { 78 | background: transparent; 79 | border: 0 none; 80 | cursor: pointer; 81 | font-family: inherit; 82 | font-size: 100%; 83 | line-height: 1.15; 84 | margin: 0; 85 | text-transform: none; 86 | } 87 | 88 | button::-moz-focus-inner { 89 | border-style: none; 90 | padding: 0; 91 | } 92 | 93 | button:-moz-focusring { 94 | outline: 1px dotted ButtonText; 95 | } 96 | 97 | button { 98 | -webkit-appearance: button; 99 | line-height: 1; 100 | margin: 0; 101 | overflow: visible; 102 | padding: 0; 103 | } 104 | 105 | button:active, 106 | select:active { 107 | outline: 0 none; 108 | } 109 | 110 | li { 111 | margin-bottom: 4px; 112 | } 113 | 114 | p { 115 | margin: var(--small-gap) 0; 116 | } 117 | 118 | .rendered-markdown h1, 119 | .rendered-markdown h2, 120 | .rendered-markdown h3, 121 | .rendered-markdown h4, 122 | .rendered-markdown h5 { 123 | font-size: 1.3rem; 124 | } 125 | 126 | /* Code */ 127 | 128 | pre, 129 | code { 130 | font-family: "Ubuntu Mono", monospace; 131 | line-height: 1.2; 132 | } 133 | 134 | pre { 135 | margin: var(--gap) 0; 136 | background-color: var(--code-background); 137 | border-radius: 1px; 138 | overflow: auto; 139 | box-shadow: var(--shadow); 140 | } 141 | 142 | pre > code, 143 | code.hljs { 144 | padding: var(--small-gap) var(--gap); 145 | background: transparent; 146 | } 147 | 148 | p code { 149 | margin: 0 2px; 150 | } 151 | 152 | /* Page layout */ 153 | 154 | .page { 155 | display: flex; 156 | } 157 | 158 | .content { 159 | margin-left: var(--sidebar-width); 160 | padding: calc(var(--header-height) + var(--gap)) var(--gap) 0 var(--gap); 161 | width: calc(100% - var(--sidebar-width)); 162 | max-width: var(--content-width); 163 | } 164 | 165 | /* Page header */ 166 | 167 | .page-header { 168 | box-shadow: var(--nav-shadow); 169 | height: var(--header-height); 170 | color: black; 171 | color: var(--hard-black); 172 | background-color: var(--pink); 173 | display: flex; 174 | padding: var(--small-gap) var(--gap); 175 | position: fixed; 176 | left: 0; 177 | right: 0; 178 | top: 0; 179 | z-index: 300; 180 | } 181 | 182 | .page-header h2 { 183 | align-items: baseline; 184 | display: flex; 185 | margin: 0; 186 | max-width: 100%; 187 | } 188 | 189 | .page-header a, 190 | .page-header a:visited { 191 | color: black; 192 | color: var(--hard-black); 193 | overflow: hidden; 194 | text-overflow: ellipsis; 195 | white-space: nowrap; 196 | } 197 | 198 | .sidebar-toggle { 199 | display: none; 200 | font-size: var(--sidebar-toggle-size); 201 | opacity: 0; 202 | transition: opacity 1s ease; 203 | } 204 | 205 | .page-header .sidebar-toggle { 206 | color: white; 207 | color: var(--white); 208 | margin: 0 var(--small-gap) 0 0; 209 | } 210 | 211 | /* Version selector */ 212 | 213 | #project-version { 214 | --half-small-gap: calc(var(--small-gap) / 2); 215 | --icon-size: .75em; 216 | flex-shrink: 0; 217 | font-size: .9rem; 218 | font-weight: normal; 219 | margin-left: var(--half-small-gap); 220 | } 221 | 222 | #project-version > span { 223 | padding-left: var(--half-small-gap); 224 | } 225 | 226 | #project-version form { 227 | align-items: center; 228 | display: inline-flex; 229 | justify-content: flex-end; 230 | } 231 | 232 | #project-version select { 233 | appearance: none; 234 | -webkit-appearance: none; 235 | padding: .6rem calc(1.3 * var(--icon-size)) .6rem var(--half-small-gap); 236 | position: relative; 237 | z-index: 1; 238 | } 239 | 240 | #project-version option { 241 | background-color: var(--code-background); 242 | } 243 | 244 | #project-version .icon { 245 | font-size: var(--icon-size); 246 | margin-left: calc(-1.65 * var(--icon-size)); 247 | } 248 | 249 | /* Module doc */ 250 | 251 | .module-name > a, 252 | .module-member-kind > a { 253 | color: inherit; 254 | } 255 | 256 | .module-name > a:hover, 257 | .module-member-kind > a:hover { 258 | text-decoration: none; 259 | } 260 | 261 | .module-name > .icon-gleam-chasse, 262 | .module-member-kind > .icon-gleam-chasse, 263 | .module-member-kind > .icon-gleam-chasse-2 { 264 | color: var(--pink); 265 | display: block; 266 | font-size: 1rem; 267 | margin: var(--small-gap) 0 0; 268 | } 269 | 270 | .module-name { 271 | color: var(--hard-black); 272 | margin: 0 0 var(--gap); 273 | font-weight: 700; 274 | } 275 | 276 | /* Sidebar */ 277 | 278 | .sidebar { 279 | background-color: var(--background); 280 | font-size: .95rem; 281 | max-height: calc(100vh - var(--header-height)); 282 | overflow-y: auto; 283 | overscroll-behavior: contain; 284 | padding-top: var(--gap); 285 | padding-bottom: calc(3 * var(--gap)); 286 | padding-left: var(--gap); 287 | position: fixed; 288 | top: var(--header-height); 289 | transition: transform .5s ease; 290 | width: var(--sidebar-width); 291 | z-index: 100; 292 | } 293 | 294 | .sidebar h2 { 295 | margin: 0; 296 | } 297 | 298 | .sidebar ul { 299 | list-style: none; 300 | margin: var(--small-gap) 0; 301 | padding: 0; 302 | } 303 | 304 | .sidebar li { 305 | line-height: 1.2; 306 | margin-bottom: 4px; 307 | } 308 | 309 | .sidebar .sidebar-toggle { 310 | color: var(--pink); 311 | font-size: calc(.8 * var(--sidebar-toggle-size)); 312 | } 313 | 314 | body.drawer-closed .label-open, 315 | body.drawer-open .label-closed { 316 | display: none; 317 | } 318 | 319 | .display-controls { 320 | display: flex; 321 | flex-wrap: wrap; 322 | margin-top: var(--small-gap); 323 | padding-right: var(--gap); 324 | } 325 | 326 | .display-controls .control { 327 | margin: .5rem 0; 328 | } 329 | 330 | .display-controls .control:not(:first-child) { 331 | margin-left: 1rem; 332 | } 333 | 334 | .toggle { 335 | align-items: center; 336 | display: flex; 337 | font-size: .96rem; 338 | } 339 | 340 | .toggle-0 .label:not(.label-0), 341 | .toggle-1 .label:not(.label-1) { 342 | display: none; 343 | } 344 | 345 | .label { 346 | display: flex; 347 | } 348 | 349 | .label .icon { 350 | margin: 0 .28rem; 351 | } 352 | 353 | /* Module members (types, functions) */ 354 | 355 | .module-members { 356 | margin-top: var(--large-gap); 357 | } 358 | 359 | .module-member-kind { 360 | font-size: 2rem; 361 | color: var(--text); 362 | } 363 | 364 | .member { 365 | margin: var(--large-gap) 0; 366 | padding-bottom: var(--gap); 367 | } 368 | 369 | .member-name { 370 | display: flex; 371 | align-items: center; 372 | justify-content: space-between; 373 | border-left: 4px solid var(--accent); 374 | padding: var(--small-gap) var(--gap); 375 | background-color: var(--accented-background); 376 | } 377 | 378 | .member-name h2 { 379 | display: flex; 380 | font-size: 1.5rem; 381 | margin: 0; 382 | } 383 | 384 | .member-name h2 a { 385 | color: var(--text); 386 | } 387 | 388 | .member-source { 389 | align-self: baseline; 390 | flex-shrink: 0; 391 | line-height: calc(1.4 * 1.5rem); 392 | margin: 0 0 0 var(--small-gap); 393 | } 394 | 395 | /* Custom type constructors */ 396 | 397 | .constructor-list { 398 | list-style: none; 399 | padding: 0; 400 | } 401 | 402 | .constructor-item { 403 | align-items: center; 404 | display: flex; 405 | } 406 | 407 | .constructor-item .icon { 408 | flex-shrink: 0; 409 | font-size: .7rem; 410 | margin: 0 .88rem; 411 | } 412 | 413 | .constructor-name { 414 | box-shadow: unset; 415 | margin: 0; 416 | } 417 | 418 | .constructor-name > code { 419 | padding: var(--small-gap); 420 | } 421 | 422 | /* Tables */ 423 | 424 | table { 425 | border-spacing: 0; 426 | border-collapse: collapse; 427 | } 428 | 429 | table td, 430 | table th { 431 | padding: 6px 13px; 432 | border: 1px solid var(--table-border); 433 | } 434 | 435 | table tr:nth-child(2n) { 436 | background-color: var(--table-background); 437 | } 438 | 439 | /* Footer */ 440 | 441 | .pride { 442 | width: 100%; 443 | display: none; 444 | flex-direction: row; 445 | position: absolute; 446 | bottom: 0; 447 | z-index: 100; 448 | } 449 | 450 | .show-pride .pride { 451 | display: flex; 452 | } 453 | 454 | .show-pride .sidebar { 455 | margin-bottom: var(--gap); 456 | } 457 | 458 | .pride div { 459 | flex: 1; 460 | text-align: center; 461 | padding: var(--tiny-gap); 462 | } 463 | 464 | .pride .white { 465 | background-color: var(--white); 466 | } 467 | .pride .pink { 468 | background-color: var(--pink); 469 | } 470 | .pride .blue { 471 | background-color: var(--boi-blue); 472 | } 473 | 474 | .pride-button { 475 | position: absolute; 476 | right: 2px; 477 | bottom: 2px; 478 | opacity: .2; 479 | font-size: .9rem; 480 | } 481 | 482 | .pride-button { 483 | text-decoration: none; 484 | cursor: default; 485 | } 486 | 487 | /* Icons */ 488 | 489 | .svg-lib { 490 | height: 0; 491 | overflow: hidden; 492 | position: absolute; 493 | width: 0; 494 | } 495 | 496 | .icon { 497 | display: inline-block; 498 | fill: currentColor; 499 | height: 1em; 500 | stroke: currentColor; 501 | stroke-width: 0; 502 | width: 1em; 503 | } 504 | 505 | .icon-gleam-chasse { 506 | width: 8.182em; 507 | } 508 | 509 | .icon-gleam-chasse-2 { 510 | width: 4.909em; 511 | } 512 | 513 | /* Pre-Wrap Option */ 514 | 515 | body.prewrap-on code, 516 | body.prewrap-on pre { 517 | white-space: pre-wrap; 518 | } 519 | 520 | /* Dark Theme Option */ 521 | 522 | body.theme-dark { 523 | /* Colour palette adapted from: 524 | * https://github.com/dustypomerleau/yarra-valley 525 | */ 526 | 527 | --argument-atom: #c651e5; 528 | --class-module: #ff89b5; 529 | --comment: #7e818b; 530 | --escape: #7cdf89; 531 | --function-call: #abb8c0; 532 | --function-definition: #8af899; 533 | --interpolation-regex: #ee37aa; 534 | --keyword-operator: #ff9d35; 535 | --number-boolean: #f14360; 536 | --object: #99c2eb; 537 | --punctuation: #4ce7ff; 538 | --string: #aecc00; 539 | 540 | --bg: #292d3e; 541 | --bg-tint-1: #3e4251; --bg-tint-2: #535664; --bg-tint-3: #696c77; --bg-tint-4: #7e818b; 542 | --bg-shade-1: #242837; --bg-shade-2: #202431; --bg-shade-3: #1c1f2b; 543 | --bg-mono-1: #33384d; --bg-mono-2: #3d435d; --bg-mono-3: #474e6c; --bg-mono-4: #51597b; 544 | 545 | --fg: #cac0a9; 546 | --fg-tint-1: #fdf2d8; --fg-tint-2: #fdf3dc; --fg-tint-3: #fdf5e0; 547 | --fg-shade-1: #e3d8be; --fg-shade-2: #cac0a9; --fg-shade-3: #b1a894; --fg-shade-4: #97907f; 548 | 549 | --orange-shade-1: #e58d2f; --orange-shade-2: #cc7d2a; --orange-shade-3: #b26d25; 550 | 551 | --taupe-mono-1: #fdf1d4; --taupe-mono-2: #fce9bc; --taupe-mono-3: #fbe1a3; 552 | 553 | /* Theme Overrides */ 554 | 555 | --accent: var(--pink); 556 | --accented-background: var(--bg-shade-1); 557 | --background: var(--bg); 558 | --code-background: var(--bg-shade-2); 559 | --hard-black: var(--taupe-mono-1); 560 | --links: var(--pink); 561 | --text: var(--taupe-mono-1); 562 | 563 | --shadow: 564 | 0 0 0 1px rgba(50, 50, 93, .075), 565 | 0 0 1px var(--fg-shade-3), 566 | 0 2px 4px -2px rgba(138, 141, 151, .2); 567 | --nav-shadow: 0 0 5px 5px rgba(0, 0, 0, .1); 568 | } 569 | 570 | body.theme-dark { 571 | background-color: var(--bg); 572 | color: var(--fg-shade-1); 573 | } 574 | 575 | body.theme-dark .page-header { 576 | background-color: var(--bg-mono-1); 577 | } 578 | 579 | body.theme-dark .page-header h2 { 580 | color: var(--fg-shade-1); 581 | } 582 | 583 | 584 | body.theme-dark .page-header a, 585 | body.theme-dark .page-header a:visited { 586 | color: var(--pink); 587 | } 588 | 589 | body.theme-dark .page-header .sidebar-toggle { 590 | color: var(--fg-shade-1); 591 | } 592 | 593 | body.theme-dark #project-version select, 594 | body.theme-dark .control { 595 | color: var(--fg-shade-1); 596 | } 597 | 598 | body.theme-dark .module-name { 599 | color: var(--taupe-mono-1); 600 | } 601 | 602 | body.theme-dark .pride { 603 | color: var(--bg-shade-3); 604 | } 605 | 606 | body.theme-dark .pride .white { 607 | background-color: var(--fg-shade-1); 608 | } 609 | 610 | body.theme-dark .pride .pink { 611 | background-color: var(--argument-atom); 612 | } 613 | 614 | body.theme-dark .pride .blue { 615 | background-color: var(--punctuation); 616 | } 617 | 618 | /* Medium and larger displays */ 619 | @media (min-width: 680px) { 620 | #prewrap-toggle { 621 | display: none; 622 | } 623 | } 624 | 625 | /* Small displays */ 626 | @media (max-width: 920px) { 627 | .page-header { 628 | padding-left: var(--small-gap); 629 | padding-right: var(--small-gap); 630 | } 631 | 632 | .page-header h2 { 633 | max-width: calc(100% - var(--sidebar-toggle-size) - var(--small-gap)); 634 | } 635 | 636 | .content { 637 | width: 100%; 638 | max-width: unset; 639 | margin-left: unset; 640 | } 641 | 642 | .sidebar { 643 | box-shadow: var(--nav-shadow); 644 | height: 100vh; 645 | max-height: unset; 646 | top: 0; 647 | transform: translate(calc(-10px - var(--sidebar-width))); 648 | z-index: 500; 649 | } 650 | 651 | body.drawer-open .sidebar { 652 | transform: translate(0); 653 | } 654 | 655 | .sidebar-toggle { 656 | display: block; 657 | opacity: 1; 658 | } 659 | 660 | .sidebar .sidebar-toggle { 661 | height: var(--sidebar-toggle-size); 662 | position: absolute; 663 | right: var(--small-gap); 664 | top: var(--small-gap); 665 | width: var(--sidebar-toggle-size); 666 | } 667 | } 668 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | string_parser 7 | 8 | 10 | 11 | 12 | 13 | 14 | 96 | 97 | 133 | 134 | 178 | 179 |
180 | 206 | 207 |
208 | 209 |

gleam-string-parser

210 |

A simple parser combinator library built in Gleam.

211 |

Quick start

212 |
# Run the eunit tests
213 | rebar3 eunit
214 | 
215 | # Run the Erlang REPL
216 | rebar3 shell
217 | 
218 |

Installation

219 |

If available in Hex 220 | this package can be installed by adding simple_parser to your rebar.config dependencies:

221 |
{deps, [
222 |     string_parser
223 | ]}.
224 | 
225 | 226 | 227 |
228 |
229 | 230 | 234 | 235 | 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 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | -------------------------------------------------------------------------------- /docs/string/parser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | string/parser - string_parser 7 | 8 | 10 | 11 | 12 | 13 | 14 | 96 | 97 | 133 | 134 | 178 | 179 |
180 | 265 | 266 |
267 | 268 |

269 | string/parser 270 | 271 |

272 | 318 | 319 | 320 | 321 |
322 |

323 | Types 324 | 325 |

326 | 327 | 328 |
329 |
330 |

331 | 332 | Error 333 | 334 |

335 | 336 | 337 |
338 |
339 |
344 |

A Parser might fail for a number of reasons, so we enumerate 345 | them here. The Custom constructor is useful when used in combination with 346 | then to give an explanation of why the parser is failing.

347 | 352 |
353 |
pub type Error {
 354 |   BadParser(String)
 355 |   Custom(String)
 356 |   EOF
 357 |   Expected(String, got: String)
 358 |   UnexpectedInput(String)
 359 | }
360 | 361 |

362 | Constructors 363 |

364 |
    365 | 366 |
  • 367 | 368 |
    BadParser(String)
    369 | 370 |
  • 371 | 372 |
  • 373 | 374 |
    Custom(String)
    375 | 376 |
  • 377 | 378 |
  • 379 | 380 |
    EOF
    381 | 382 |
  • 383 | 384 |
  • 385 | 386 |
    Expected(String, got: String)
    387 | 388 |
  • 389 | 390 |
  • 391 | 392 |
    UnexpectedInput(String)
    393 | 394 |
  • 395 | 396 |
397 | 398 |
399 |
400 | 401 |
402 |
403 |

404 | 405 | Parser 406 | 407 |

408 | 409 | 410 |
411 |
412 |
417 |

A Parser is something that takes a string and attempts to transform it 418 | into something else, often consuming some or all of the input in the process.

419 |

Parsers can be combined (that’s why they’re called parser combinators) to 420 | parse more complex structures. You could write a JSON parser, or a CSV parser, 421 | or even do fancy things like parsing and evaluating simple maths expressions.

422 | 427 |
428 |
pub opaque type Parser(a)
429 | 430 | 431 |
432 |
433 | 434 |
435 | 436 | 437 | 438 | 439 | 440 | 441 |
442 |

443 | Functions 444 | 445 |

446 | 447 |
448 |
449 |

450 | 451 | any 452 | 453 |

454 | 455 | 456 |
457 |
pub fn any() -> Parser(String)
458 |
463 |

Parse a single grapheme from the input, it could be anything! If the input 464 | is an empty string, this will fail with the EOF error.

465 |
466 | Example: 467 |
 import gleam/result.{Ok, Error}
 468 |  import gleam/should
 469 |  import string/parser
 470 | 
 471 |  pub fn example () {
 472 |      let parser = parser.any()
 473 | 
 474 |      parser.run("Hello world", parser)
 475 |          |> should.equal(Ok("H"))
 476 |  }
 477 | 
478 |
479 | 484 |
485 |
486 | 487 |
488 |
489 |

490 | 491 | drop 492 | 493 |

494 | 495 | 496 |
497 |
pub fn drop(keeper: Parser(a), ignorer: Parser(b)) -> Parser(a)
498 |
503 |

This is one of two combinators used in conjuction with succeed 504 | (the other being keep) that come together to form a nice pipeline 505 | API for parsing.

506 |

This combinator runs two parsers in sequence, and then keeps the result of 507 | the first parser while ignoring the result of the second. This allows us to 508 | write parsers that enforce a particular structure from the input without 509 | needing to do anything with the result of those structural parsers.

510 |

Think of parsing JSON, for example. We need to parse opening and closing curly 511 | braces to know it’s a valid object, and we need to parse the separating colon 512 | to differentiate between key and value. We need to parse these things, but 513 | they’re structural, we might want to parse a key/value pair into a Gleam 514 | tuple; we don’t care about the curly braces or colons but we need to know 515 | they’re there.

516 |
517 | Example: 518 |
 import gleam/result.{Ok, Error}
 519 |  import gleam/should
 520 |  import gleam/string.{String}
 521 |  import gleam/int
 522 |  import string/parser.{UnexpectedInput}
 523 | 
 524 |  pub fn example () {
 525 |      let digit_parser = parser.any() |> parser.then(fn (c) {
 526 |          case int.parse(c) {
 527 |              Ok(num) -> parser.succeed(num)
 528 |              Error(_) -> parser.fail_with(UnexpectedInput)
 529 |          }
 530 |      })
 531 |      
 532 |      let parser = parser.succeed2(fn (x, y) { x + y })
 533 |          |> parser.keep(digit_parser)
 534 |          |> parser.drop(parser.spaces())
 535 |          |> parser.drop(parser.string("+"))
 536 |          |> parser.drop(parser.spaces())
 537 |          |> parser.keep(digit_parser)
 538 | 
 539 |      parser.run("1 + 3", parser)
 540 |          |> should.equal(Ok(4))
 541 |  }
 542 | 
543 |
544 | 549 |
550 |
551 | 552 |
553 |
554 |

555 | 556 | eof 557 | 558 |

559 | 560 | 561 |
562 |
pub fn eof() -> Parser(Nil)
563 |
568 |

This parser only succeeds when the input is an empty string. Why is that 569 | useful? You can use this in combination with your other parsers to ensure 570 | that you’ve consumed all the input.

571 |
572 | Example: 573 |
 import gleam/function
 574 |  import gleam/result.{Ok, Error}
 575 |  import gleam/should
 576 |  import string/parser.{Expected}
 577 | 
 578 |  pub fn example () {
 579 |      let parser = parser.succeed(function.identity)
 580 |          |> parser.keep(parser.string("Hello"))
 581 |          |> parser.drop(parser.eof())
 582 | 
 583 |      parser.run("Hello", parser)
 584 |          |> should.equal(Ok("Hello"))
 585 | 
 586 |      parser.run("Hello world", parser)
 587 |          |> should.equal(Error(Expected("End of file", got: " world")))
 588 |  }
 589 | 
590 |
591 | 596 |
597 |
598 | 599 |
600 |
601 |

602 | 603 | fail 604 | 605 |

606 | 607 | 608 |
609 |
pub fn fail(message: String) -> Parser(a)
610 |
615 |

Create a Parser that always fails regardless of the input. This 616 | uses the Custom error constructor of the Error type. If you’d 617 | like to return a more specific error, you can look at fail_with 618 | instead.

619 |
620 | Example: 621 |
 import gleam/result.{Ok, Error}
 622 |  import gleam/should
 623 |  import gleam/string
 624 |  import string/parser.{Custom}
 625 | 
 626 |  pub fn example () {
 627 |      let is_not_space = fn (c) { c != " " }
 628 |      let parse_four_letter_word = fn (s) {
 629 |          case string.length(s) {
 630 |              True -> 
 631 |                  parser.succeed(s)
 632 | 
 633 |              False -> 
 634 |                  parser.fail("Expected a four letter word.")
 635 |          }
 636 |      }
 637 |      let parser = parser.take_while(is_not_space)
 638 |          |> parser.then(parse_four_letter_word)
 639 | 
 640 |      parser.run("Hello world", parser)
 641 |          |> should.equal(Error(Custom("Expected a four letter word.")))
 642 |  }
 643 | 
644 |
645 | 650 |
651 |
652 | 653 |
654 |
655 |

656 | 657 | fail_with 658 | 659 |

660 | 661 | 662 |
663 |
pub fn fail_with(error: Error) -> Parser(a)
664 |
669 |

Create a Parser that always fails regardless of the input. Unlike 670 | fail, you can use any of the constructors for the Error 671 | type here.

672 |
673 | Example: 674 |
 import gleam/result.{Ok, Error}
 675 |  import gleam/should
 676 |  import gleam/string
 677 |  import string/parser.{Expected}
 678 | 
 679 |  pub fn example () {
 680 |      let is_not_space = fn (c) { c != " " }
 681 |      let parse_four_letter_word = fn (s) {
 682 |          case string.length(s) {
 683 |              True -> 
 684 |                  parser.succeed(s)
 685 | 
 686 |              False ->
 687 |                  parser.fail_with(Expected("A four letter word", got: s))
 688 |          }
 689 |      }
 690 |      let parser = parser.take_while(is_not_space)
 691 |          |> parser.then(parse_four_letter_word)
 692 | 
 693 |      parser.run("Hello world", parser)
 694 |          |> should.equal(Error(Expected("A four letter word", got: s))
 695 |  }
 696 | 
697 |
698 | 703 |
704 |
705 | 706 |
707 |
708 |

709 | 710 | keep 711 | 712 |

713 | 714 | 715 |
716 |
pub fn keep(
 717 |   mapper: Parser(fn(a) -> b),
 718 |   parser: Parser(a),
 719 | ) -> Parser(b)
720 |
725 |

This is one of two combinators used in conjuction with succeed 726 | (the other being drop) that come together to form a nice pipeline 727 | API for parsing.

728 |

The first argument is a function wrapped up in a Parser, usually 729 | created using succeed. The second argument is a parser for the 730 | value we want to keep. This parser combines the two by applying that value 731 | to the function.

732 |

When you use one of the succeed{N} functions such as succeed2 733 | you’ll get back a [Parser] for a function. You might then work out that you 734 | can use keep again, and voila we have a neat declarative pipline parser 735 | that describes what results we want to keep and how.

736 |

The explanation is a bit wordy and compicated, but its useage is intuitive. 737 | If you’ve been looking at the rest of the docs for this package, you’ll already 738 | have seen keep used all over the place.

739 |
740 | Example: 741 |
 import gleam/result.{Ok, Error}
 742 |  import gleam/should
 743 |  import gleam/string.{String}
 744 |  import gleam/int
 745 |  import string/parser.{UnexpectedInput}
 746 | 
 747 |  pub fn example () {
 748 |      let digit_parser = parser.any() |> parser.then(fn (c) {
 749 |          case int.parse(c) {
 750 |              Ok(num) -> parser.succeed(num)
 751 |              Error(_) -> parser.fail_with(UnexpectedInput)
 752 |          }
 753 |      })
 754 |      
 755 |      let parser = parser.succeed2(fn (x, y) { x + y })
 756 |          |> parser.keep(digit_parser)
 757 |          |> parser.drop(parser.spaces())
 758 |          |> parser.drop(parser.string("+"))
 759 |          |> parser.drop(parser.spaces())
 760 |          |> parser.keep(digit_parser)
 761 | 
 762 |      parser.run("1 + 3", parser)
 763 |          |> should.equal(Ok(4))
 764 |  }
 765 | 
766 |
767 | 772 |
773 |
774 | 775 |
776 |
777 |

778 | 779 | map 780 | 781 |

782 | 783 | 784 |
785 |
pub fn map(parser: Parser(a), f: fn(a) -> b) -> Parser(b)
786 |
791 |

A combinator that transforms a parsed value by applying a function to it and 792 | returning a new Parser with that transformed value. This follows 793 | the same pattern as gleam/option.map 794 | or gleam/result.map.

795 |
796 | import gleam/result.{Ok, Error} 797 | import gleam/should 798 | import string/parser 799 |
 pub fn example () {
 800 |      let parser = parser.any() |> parser.map(string.repeat(5))
 801 | 
 802 |      parser.run("Hello world", parser)
 803 |          |> should.equal(Ok("HHHHH"))
 804 | 
 805 |      parser.run("", parser)
 806 |          |> should.equal(Error(parser.EOF))
 807 |  }
 808 | 
809 |
810 | 815 |
816 |
817 | 818 |
819 |
820 |

821 | 822 | map2 823 | 824 |

825 | 826 | 827 |
828 |
pub fn map2(
 829 |   parser_a: Parser(a),
 830 |   parser_b: Parser(b),
 831 |   f: fn(a, b) -> c,
 832 | ) -> Parser(c)
833 |
838 |

A combinator that combines two parsed values by applying a function to them 839 | both and returning a new Parser containing the combined value.

840 |

Fun fact, keep is defined using this combinator.

841 |
842 | Example: 843 |
 import gleam/result.{Ok, Error}
 844 |  import gleam/should
 845 |  import gleam/string
 846 |  import string/parser
 847 | 
 848 |  pub fn example () {
 849 |      let parser = parser.map2(
 850 |          parser.string("Hello"),
 851 |          parser.string("world"),
 852 |          fn (hello, world) {
 853 |              string.concat([ hello, " ", world ])
 854 |          }
 855 |      )
 856 | 
 857 |      parser.run("Helloworld", parser)
 858 |          |> should.equal(Ok("Hello world"))
 859 |  }
 860 | 
861 |
862 | 867 |
868 |
869 | 870 |
871 |
872 |

873 | 874 | one_of 875 | 876 |

877 | 878 | 879 |
880 |
pub fn one_of(parsers: List(Parser(a))) -> Parser(a)
881 |
886 |

A combinator that tries a list of parsers in sequence until one succeeds. If 887 | you pass in an empty list this parser will fail with the BadParser constructor 888 | of the Error type.

889 |
890 | import gleam/result.{Ok, Error} 891 | import gleam/should 892 | import gleam/string 893 | import string/parser 894 |
 pub fn example () {
 895 |      let beam_lang = parser.oneOf(
 896 |          parser.string("Erlang"),
 897 |          parser.string("Elixir"),
 898 |          parser.string("Gleam")
 899 |      )
 900 |      let parser = parser.succeed2(string.append)
 901 |          |> parser.keep(parser.string("Hello "))
 902 |          |> parser.keep(beam_lang)
 903 | 
 904 |      parser.run("Hello Gleam", parser)
 905 |          |> should.equal(Ok("Hello Gleam"))
 906 |  }
 907 | 
908 |
909 | 914 |
915 |
916 | 917 |
918 |
919 |

920 | 921 | run 922 | 923 |

924 | 925 | 926 |
927 |
pub fn run(input: String, parser: Parser(a)) -> Result(a, Error)
928 |
933 |

Run a parser and get back its result. If the supplied parser doesn’t entirely 934 | consume its input, the remaining string is dropped, never to be seen again.

935 |
936 | Example: 937 |
 import gleam/result.{Ok, Error}
 938 |  import gleam/should
 939 |  import string/parser
 940 | 
 941 |  pub fn example () {
 942 |      let parser = parser.any() |> parser.map(string.repeat(5))
 943 | 
 944 |      parser.run("Hello world", parser)
 945 |          |> should.equal(Ok("HHHHH"))
 946 | 
 947 |      parser.run("", parser)
 948 |          |> should.equal(Error(parser.EOF))
 949 |  }
 950 | 
951 |
952 | 957 |
958 |
959 | 960 |
961 |
962 |

963 | 964 | spaces 965 | 966 |

967 | 968 | 969 |
970 |
pub fn spaces() -> Parser(Nil)
971 |
976 |

Parse zero or more spaces in sequence.

977 |
978 | Example: 979 |
 import gleam/result.{Ok, Error}
 980 |  import gleam/should
 981 |  import string/parser.{Expected}
 982 | 
 983 |  pub fn example () {
 984 |      let parser = parser.succeed("No spaces required")
 985 |          |> parser.drop(parser.string("Hello"))
 986 |          |> parser.drop(parser.spaces())
 987 |          |> parser.drop(parser.string("world"))
 988 | 
 989 |      parser.run("Helloworld", parser)
 990 |          |> should.equal(Ok("No spaces required"))
 991 | 
 992 |      let is_space = fn (c) { c == " " }
 993 |      let parser = parser.succeed("At least one space required")
 994 |          |> parser.drop(parser.string("Hello"))
 995 |          |> parser.drop(parser.take_if(is_space))
 996 |          |> parser.drop(parser.spaces())
 997 |          |> parser.drop(parser.string("world"))
 998 | 
 999 |      parser.run("Helloworld", parser)
1000 |          |> should.equal(Error(Expected(" ", got: "world")))
1001 | 
1002 |      parser.run("Hello world", parser)
1003 |          |> should.equal(Ok("At least one space required"))
1004 |  }
1005 | 
1006 |
1007 | 1012 |
1013 |
1014 | 1015 |
1016 |
1017 |

1018 | 1019 | string 1020 | 1021 |

1022 | 1023 | 1024 |
1025 |
pub fn string(value: String) -> Parser(String)
1026 |
1031 |

Parse an exact string from the input. If you were writing a programming 1032 | language parser, you might use this to parse keywords or symbols.

1033 |
1034 | Example: 1035 |
 import gleam/result.{Ok, Error}
1036 |  import gleam/should
1037 |  import gleam/string.{String}
1038 |  import string/parser.{Expected}
1039 | 
1040 |  type VariableDeclaration {
1041 |      VariableDeclaration(var: String)
1042 |  }
1043 | 
1044 |  pub fn example () {
1045 |      let is_not_space = fn (c) { c != " " }
1046 |      let parser = parser.succeed(VariableDeclaration)
1047 |          |> parser.drop(parser.string("var"))
1048 |          |> parser.drop(parser.spaces())
1049 |          |> parser.keep(parser.take_while(is_not_space))
1050 |          |> parser.drop(parser.string(";"))
1051 | 
1052 |      parser.run("var x;", parser)
1053 |          |> should.equal(Ok(VariableDeclaration(var: "x")))
1054 | 
1055 |      parser.run("let x;", parser)
1056 |          |> should.equal(Error(Expected("var", got: "let x;")))
1057 |  }
1058 | 
1059 |
1060 | 1065 |
1066 |
1067 | 1068 |
1069 |
1070 |

1071 | 1072 | succeed 1073 | 1074 |

1075 | 1076 | 1077 |
1078 |
pub fn succeed(value: a) -> Parser(a)
1079 |
1084 |

Ignore the input string and succeed with the given value. Commonly used in 1085 | combination with keep and drop by passing in a function 1086 | to succeed and then using keep to call that function with the 1087 | result of another parser.

1088 |

If that sounds a bit baffling, see the example below. It is a rewrite of 1089 | the example used for Parser but one that is able to parse 1090 | and ignore leading whitespace from the input.

1091 |
1092 | Example: 1093 |
 import gleam/result.{Ok, Error}
1094 |  import gleam/should
1095 |  import string/parser
1096 | 
1097 |  pub fn example () {
1098 |      let parser = parser.succeed(string.repeat(5))
1099 |          |> parser.drop(parser.spaces())
1100 |          |> parser.keep(parser.any())
1101 | 
1102 |      parser.run("Hello world", parser)
1103 |          |> should.equal(Ok("HHHHH"))
1104 | 
1105 |      parser.run("   Hello world", parser)
1106 |          |> should.equal(Ok("HHHHH"))
1107 |  }
1108 | 
1109 |
1110 | 1115 |
1116 |
1117 | 1118 |
1119 |
1120 |

1121 | 1122 | succeed2 1123 | 1124 |

1125 | 1126 | 1127 |
1128 |
pub fn succeed2(f: fn(a, b) -> c) -> Parser(fn(a) -> fn(b) -> c)
1129 |
1134 |

Like succeed but for a function that takes four arguments. 1135 | Functions in Gleam aren’t automatically curried like they are in languages 1136 | like Elm or Haskell, and that is problematic for our succeed 1137 | parser to work correctly. To address this, succeed2 takes a 1138 | function expecting two arguments, and calls function.curry2 to turn 1139 | it into a sequence functions that are expecting one argument.

1140 |

You could implement this yourself by doing function.curry2(f) |> parser.succeed 1141 | where f is the two-argument function you want to use.

1142 |
1143 | Example: 1144 |
 import gleam/result.{Ok, Error}
1145 |  import gleam/should
1146 |  import string/parser
1147 | 
1148 |  pub fn example () {
1149 |      let is_not_space = fn (c) { c != " " }
1150 |      let parser = parser.succeed2(string.append)
1151 |          |> parser.keep(parser.take_while(is_not_space))
1152 |          |> parser.drop(parser.spaces())
1153 |          |> parser.keep(parser.take_while(is_not_space))
1154 | 
1155 |      parser.run("Hello world", parser)
1156 |          |> should.equal(Ok("Helloworld"))
1157 |  }
1158 | 
1159 |
1160 | 1165 |
1166 |
1167 | 1168 |
1169 |
1170 |

1171 | 1172 | succeed3 1173 | 1174 |

1175 | 1176 | 1177 |
1178 |
pub fn succeed3(
1179 |   f: fn(a, b, c) -> d,
1180 | ) -> Parser(fn(a) -> fn(b) -> fn(c) -> d)
1181 |
1186 |

Like succeed but for a function that takes four arguments. 1187 | Functions in Gleam aren’t automatically curried like they are in languages 1188 | like Elm or Haskell, and that is problematic for our succeed 1189 | parser to work correctly. To address this, succeed3 takes a 1190 | function expecting three arguments, and calls function.curry4 to turn 1191 | it into a sequence functions that are expecting one argument.

1192 |

For an example of how this is used, take a look at the examples given for 1193 | succeed and succeed2.

1194 | 1199 |
1200 |
1201 | 1202 |
1203 |
1204 |

1205 | 1206 | succeed4 1207 | 1208 |

1209 | 1210 | 1211 |
1212 |
pub fn succeed4(
1213 |   f: fn(a, b, c, d) -> e,
1214 | ) -> Parser(fn(a) -> fn(b) -> fn(c) -> fn(d) -> e)
1215 |
1220 |

Like succeed but for a function that takes four arguments. 1221 | Functions in Gleam aren’t automatically curried like they are in languages 1222 | like Elm or Haskell, and that is problematic for our succeed 1223 | parser to work correctly. To address this, succeed4 takes a 1224 | function expecting four arguments, and calls function.curry4 to turn 1225 | it into a sequence functions that are expecting one argument.

1226 |

For an example of how this is used, take a look at the examples given for 1227 | succeed and succeed2.

1228 | 1233 |
1234 |
1235 | 1236 |
1237 |
1238 |

1239 | 1240 | take_if 1241 | 1242 |

1243 | 1244 | 1245 |
1246 |
pub fn take_if(predicate: fn(String) -> Bool) -> Parser(String)
1247 |
1252 |

Pop a grapheme off the input and test it against some predicate function. If 1253 | it passes then succeed with that grapheme, otherwise fail with the 1254 | UnexpectedInput constructor of the Error type. This is in 1255 | contrast to take_while which will always succeed even if no 1256 | graphemes pass the predicate.

1257 |
1258 | Example: 1259 |
 import gleam/result.{Ok, Error}
1260 |  import gleam/should
1261 |  import string/parser.{UnexpectedInput, EOF}
1262 | 
1263 |  pub fn example () {
1264 |      let is_digit = fn (c) {
1265 |          case c {
1266 |              "0" | "1" | "2" | "3" | "4" -> True
1267 |              "5" | "6" | "7" | "8" | "9" -> True
1268 |              _ -> False
1269 |          }
1270 |      }
1271 |      let parser = parser.take_while(is_digit)
1272 | 
1273 |      parser.run("1337", parser)
1274 |          |> should.equal(Ok("1"))
1275 | 
1276 |      parser.run("Hello world", parser)
1277 |          |> should.equal(Error(UnexpectedInput))
1278 | 
1279 |      parser.run("", parser)
1280 |          |> should.equal(Error(EOF))
1281 |  }
1282 | 
1283 |
1284 | 1289 |
1290 |
1291 | 1292 |
1293 |
1294 |

1295 | 1296 | take_while 1297 | 1298 |

1299 | 1300 | 1301 |
1302 |
pub fn take_while(
1303 |   predicate: fn(String) -> Bool,
1304 | ) -> Parser(String)
1305 |
1310 |

Pop a grapheme off the input string and test it against some predicate function. 1311 | If it passes, pop the next grapheme off and so on until the predicate test 1312 | fails. Join all the passing graphemes back together into a single string and 1313 | succeed with that result.

1314 |

This parser always succeeds. If you use [take_while`](#take_while) to 1315 | parse an empty string, or if no graphemes pass the predicate, this will succeed 1316 | with an empty string of its own.

1317 |
1318 | Example: 1319 |
 import gleam/result.{Ok, Error}
1320 |  import gleam/should
1321 |  import string/parser
1322 | 
1323 |  pub fn example () {
1324 |      let is_digit = fn (c) {
1325 |          case c {
1326 |              "0" | "1" | "2" | "3" | "4" -> True
1327 |              "5" | "6" | "7" | "8" | "9" -> True
1328 |              _ -> False
1329 |          }
1330 |      }
1331 |      let parser = parser.take_while(is_digit)
1332 | 
1333 |      parser.run("1337", parser)
1334 |          |> should.equal(Ok("1337"))
1335 | 
1336 |      parser.run("Hello world", parser)
1337 |          |> should.equal(Ok(""))
1338 | 
1339 |      parser.run("", parser)
1340 |          |> should.equal(Ok(""))
1341 |  }
1342 | 
1343 |
1344 | 1349 |
1350 |
1351 | 1352 |
1353 |
1354 |

1355 | 1356 | then 1357 | 1358 |

1359 | 1360 | 1361 |
1362 |
pub fn then(
1363 |   parser: Parser(a),
1364 |   f: fn(a) -> Parser(b),
1365 | ) -> Parser(b)
1366 |
1371 |

A combinator that can take the result of one parser, and use it to create 1372 | a new parser. This follows the same pattern as 1373 | gleam/option.then or 1374 | gleam/result.then.

1375 |

This is useful if you want to transform or validate a parsed value and fail 1376 | if something is wrong.

1377 |
1378 | import gleam/int 1379 | import gleam/result.{Ok, Error} 1380 | import gleam/should 1381 | import string/parser.{UnexpectedInput} 1382 |
 pub fn example () {
1383 |      let is_digit = fn (c) {
1384 |          case c {
1385 |              "0" | "1" | "2" | "3" | "4" -> True
1386 |              "5" | "6" | "7" | "8" | "9" -> True
1387 |              _ -> False
1388 |          }
1389 |      }
1390 |      let parse_digits = fn (digits) {
1391 |          case int.parse(digits) {
1392 |              Ok(num) ->
1393 |                  parser.succeed(num)
1394 | 
1395 |              Error(_) ->
1396 |                  parser.fail_with(UnexpectedInput)
1397 |      }
1398 | 
1399 |      let parser = parser.take_while(is_digit)
1400 |          |> parser.then(parse_digits)
1401 | 
1402 |      parser.run("1337", parser)
1403 |          |> should.equal(Ok(1337))
1404 | 
1405 |      parser.run("Hello world", parser)
1406 |          |> should.equal(Error(UnexpectedInput))
1407 |  }
1408 | 
1409 |
1410 | 1415 |
1416 |
1417 | 1418 |
1419 | 1420 | 1421 |
1422 |
1423 | 1424 | 1428 | 1429 | 1436 | 1437 | 1438 | 1439 | 1440 | 1441 | 1442 | 1443 | 1444 | 1445 | 1446 | 1447 | 1448 | 1449 | 1450 | 1451 | 1452 | 1453 | 1454 | 1455 | 1456 | 1457 | 1458 | 1459 | 1460 | 1461 | 1462 | 1463 | 1464 | 1465 | 1466 | 1467 | 1468 | 1469 | 1470 | 1471 | 1472 | 1473 | 1474 | 1475 | 1476 | -------------------------------------------------------------------------------- /gleam.toml: -------------------------------------------------------------------------------- 1 | name = "string_parser" 2 | version = "1.1.0" 3 | 4 | # Fill out these fields if you intend to generate HTML documentation or publish 5 | # your project to the Hex package manager. 6 | # 7 | # licences = ["Apache-2.0"] 8 | # description = "A simple parser combinator library written in Gleam." 9 | # repository = { type = "github", user = "username", repo = "project" } 10 | # links = [{ title = 'GitHub', href = 'https://github.com/pd-andy/gleam-string-parser' }] 11 | 12 | [dependencies] 13 | gleam_stdlib = "~> 0.19" 14 | 15 | [dev-dependencies] 16 | gleeunit = "~> 0.6" -------------------------------------------------------------------------------- /manifest.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by Gleam 2 | # You typically do not need to edit this file 3 | 4 | packages = [ 5 | { name = "gleam_stdlib", version = "0.19.3", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "CA579C2FF0621E93FF6EFEF61BAFFCF0505732BA073F616E28878042F1A1F401" }, 6 | { name = "gleeunit", version = "0.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "5BF486C3E135B7F5ED8C054925FC48E5B2C79016A39F416FD8CF2E860520EE55" }, 7 | ] 8 | 9 | [requirements] 10 | gleam_stdlib = "~> 0.19" 11 | gleeunit = "~> 0.6" 12 | -------------------------------------------------------------------------------- /src/string/parser.gleam: -------------------------------------------------------------------------------- 1 | //// 2 | //// * **Types** 3 | //// * [`Parser`](#Parser) 4 | //// * [`Error`](#Error) 5 | //// * **Primitive parsers** 6 | //// * [`any`](#any) 7 | //// * [`eof`](#eof) 8 | //// * [`string`](#string) 9 | //// * [`spaces`](#spaces) 10 | //// * [`whitespace`](#whitespace) 11 | //// * **Working with wrapper types** 12 | //// * [`optional`](#optional) 13 | //// * [`from_option`](#from_option) 14 | //// * [`from_result`](#from_result) 15 | //// * **Ignoring input** 16 | //// * [`succeed`](#succeed) 17 | //// * [`succeed2`](#succeed2) 18 | //// * [`succeed3`](#succeed3) 19 | //// * [`succeed4`](#succeed4) 20 | //// * [`fail`](#fail) 21 | //// * [`fail_with`](#fail_with) 22 | //// * **Chaining parsers** 23 | //// * [`keep`](#keep) 24 | //// * [`drop`](#drop) 25 | //// * **Combinators** 26 | //// * [`map`](#map) 27 | //// * [`map2`](#map2) 28 | //// * [`then`](#then) 29 | //// * [`lazy`](#lazy) 30 | //// * [`one_of`](#one_of) 31 | //// * **Predicate parsers** 32 | //// * [`take_if`](#take_if) 33 | //// * [`take_while`](#take_while) 34 | //// * [`take_if_and_while`](#take_if_and_while) 35 | //// 36 | 37 | import gleam/bool 38 | import gleam/float 39 | import gleam/function 40 | import gleam/int 41 | import gleam/list.{Continue, Stop} 42 | import gleam/option.{None, Option, Some} 43 | import gleam/pair 44 | import gleam/result 45 | import gleam/string 46 | 47 | // TYPES ----------------------------------------------------------------------- 48 | ///
49 | /// 50 | /// Spot a typo? Open an issue! 51 | /// 52 | ///
53 | /// 54 | /// A `Parser` is something that takes a string and attempts to transform it 55 | /// into something else, often consuming some or all of the input in the process. 56 | /// 57 | /// Parsers can be combined (that's why they're called parser combinators) to 58 | /// parse more complex structures. You could write a JSON parser, or a CSV parser, 59 | /// or even do fancy things like parsing and evaluating simple maths expressions. 60 | /// 61 | ///
62 | /// 63 | /// Back to top ↑ 64 | /// 65 | ///
66 | /// 67 | pub opaque type Parser(a) { 68 | Parser(fn(String) -> Result(#(a, String), Error)) 69 | } 70 | 71 | ///
72 | /// 73 | /// Spot a typo? Open an issue! 74 | /// 75 | ///
76 | /// 77 | /// A [`Parser`](#Parser) might fail for a number of reasons, so we enumerate 78 | /// them here. The `Custom` constructor is useful when used in combination with 79 | /// [`then`](#then) to give an explanation of why the parser is failing. 80 | /// 81 | /// 82 | ///
83 | /// 84 | /// Back to top ↑ 85 | /// 86 | ///
87 | /// 88 | pub type Error { 89 | BadParser(String) 90 | Custom(String) 91 | EOF 92 | Expected(String, got: String) 93 | UnexpectedInput(String) 94 | } 95 | 96 | // RUNNING PARSERS ------------------------------------------------------------- 97 | ///
98 | /// 99 | /// Spot a typo? Open an issue! 100 | /// 101 | ///
102 | /// 103 | /// Run a parser and get back its result. If the supplied parser doesn't entirely 104 | /// consume its input, the remaining string is dropped, never to be seen again. 105 | /// 106 | ///
107 | /// Example: 108 | /// 109 | /// import gleam/result.{Ok, Error} 110 | /// import gleam/should 111 | /// import string/parser 112 | /// 113 | /// pub fn example () { 114 | /// let parser = parser.any() |> parser.map(string.repeat(5)) 115 | /// 116 | /// parser.run("Hello world", parser) 117 | /// |> should.equal(Ok("HHHHH")) 118 | /// 119 | /// parser.run("", parser) 120 | /// |> should.equal(Error(parser.EOF)) 121 | /// } 122 | ///
123 | /// 124 | ///
125 | /// 126 | /// Back to top ↑ 127 | /// 128 | ///
129 | /// 130 | pub fn run(input: String, parser: Parser(a)) -> Result(a, Error) { 131 | runwrap(parser, input) 132 | |> result.map(pair.first) 133 | } 134 | 135 | /// A portmanteau of "run" and "unwrap", not "run" and "wrap". Unwraps a parser 136 | /// function and then runs it against some input. Used internally to compensate 137 | /// for the lack of function-argument pattern matching. 138 | fn runwrap(parser: Parser(a), input: String) -> Result(#(a, String), Error) { 139 | let Parser(p) = parser 140 | p(input) 141 | } 142 | 143 | // BASIC PARSERS --------------------------------------------------------------- 144 | ///
145 | /// 146 | /// Spot a typo? Open an issue! 147 | /// 148 | ///
149 | /// 150 | /// Ignore the input string and succeed with the given value. Commonly used in 151 | /// combination with [`keep`](#keep) and [`drop`](#drop) by passing in a _function_ 152 | /// to succeed and then using [`keep`](#keep) to call that function with the 153 | /// result of another parser. 154 | /// 155 | /// If that sounds a bit baffling, see the example below. It is a rewrite of 156 | /// the example used for [`Parser`](#Parser) but one that is able to parse 157 | /// _and ignore_ leading whitespace from the input. 158 | /// 159 | ///
160 | /// Example: 161 | /// 162 | /// import gleam/result.{Ok, Error} 163 | /// import gleam/should 164 | /// import string/parser 165 | /// 166 | /// pub fn example () { 167 | /// let parser = parser.succeed(string.repeat(5)) 168 | /// |> parser.drop(parser.spaces()) 169 | /// |> parser.keep(parser.any()) 170 | /// 171 | /// parser.run("Hello world", parser) 172 | /// |> should.equal(Ok("HHHHH")) 173 | /// 174 | /// parser.run(" Hello world", parser) 175 | /// |> should.equal(Ok("HHHHH")) 176 | /// } 177 | ///
178 | /// 179 | ///
180 | /// 181 | /// Back to top ↑ 182 | /// 183 | ///
184 | /// 185 | pub fn succeed(value: a) -> Parser(a) { 186 | Parser(fn(input) { Ok(#(value, input)) }) 187 | } 188 | 189 | ///
190 | /// 191 | /// Spot a typo? Open an issue! 192 | /// 193 | ///
194 | /// 195 | /// Like [`succeed`](#succeed) but for a function that takes four arguments. 196 | /// Functions in Gleam aren't automatically _curried_ like they are in languages 197 | /// like Elm or Haskell, and that is problematic for our [`succeed`](#succeed) 198 | /// parser to work correctly. To address this, [`succeed2`](#succeed2) takes a 199 | /// function expecting **two** arguments, and calls `function.curry2` to turn 200 | /// it into a sequence functions that are expecting one argument. 201 | /// 202 | /// You could implement this yourself by doing `function.curry2(f) |> parser.succeed` 203 | /// where `f` is the two-argument function you want to use. 204 | /// 205 | ///
206 | /// Example: 207 | /// 208 | /// import gleam/result.{Ok, Error} 209 | /// import gleam/should 210 | /// import string/parser 211 | /// 212 | /// pub fn example () { 213 | /// let is_not_space = fn (c) { c != " " } 214 | /// let parser = parser.succeed2(string.append) 215 | /// |> parser.keep(parser.take_while(is_not_space)) 216 | /// |> parser.drop(parser.spaces()) 217 | /// |> parser.keep(parser.take_while(is_not_space)) 218 | /// 219 | /// parser.run("Hello world", parser) 220 | /// |> should.equal(Ok("Helloworld")) 221 | /// } 222 | ///
223 | /// 224 | ///
225 | /// 226 | /// Back to top ↑ 227 | /// 228 | ///
229 | /// 230 | pub fn succeed2(f: fn(a, b) -> c) -> Parser(fn(a) -> fn(b) -> c) { 231 | function.curry2(f) 232 | |> succeed 233 | } 234 | 235 | ///
236 | /// 237 | /// Spot a typo? Open an issue! 238 | /// 239 | ///
240 | /// 241 | /// Like [`succeed`](#succeed) but for a function that takes four arguments. 242 | /// Functions in Gleam aren't automatically _curried_ like they are in languages 243 | /// like Elm or Haskell, and that is problematic for our [`succeed`](#succeed) 244 | /// parser to work correctly. To address this, [`succeed3`](#succeed3) takes a 245 | /// function expecting **three** arguments, and calls `function.curry4` to turn 246 | /// it into a sequence functions that are expecting one argument. 247 | /// 248 | /// For an example of how this is used, take a look at the examples given for 249 | /// [`succeed`](#succeed) and [`succeed2`](#succeed2). 250 | /// 251 | ///
252 | /// 253 | /// Back to top ↑ 254 | /// 255 | ///
256 | /// 257 | pub fn succeed3(f: fn(a, b, c) -> d) -> Parser(fn(a) -> fn(b) -> fn(c) -> d) { 258 | function.curry3(f) 259 | |> succeed 260 | } 261 | 262 | ///
263 | /// 264 | /// Spot a typo? Open an issue! 265 | /// 266 | ///
267 | /// 268 | /// Like [`succeed`](#succeed) but for a function that takes four arguments. 269 | /// Functions in Gleam aren't automatically _curried_ like they are in languages 270 | /// like Elm or Haskell, and that is problematic for our [`succeed`](#succeed) 271 | /// parser to work correctly. To address this, [`succeed4`](#succeed4) takes a 272 | /// function expecting **four** arguments, and calls `function.curry4` to turn 273 | /// it into a sequence functions that are expecting one argument. 274 | /// 275 | /// For an example of how this is used, take a look at the examples given for 276 | /// [`succeed`](#succeed) and [`succeed2`](#succeed2). 277 | /// 278 | ///
279 | /// 280 | /// Back to top ↑ 281 | /// 282 | ///
283 | /// 284 | pub fn succeed4( 285 | f: fn(a, b, c, d) -> e, 286 | ) -> Parser(fn(a) -> fn(b) -> fn(c) -> fn(d) -> e) { 287 | function.curry4(f) 288 | |> succeed 289 | } 290 | 291 | ///
292 | /// 293 | /// Spot a typo? Open an issue! 294 | /// 295 | ///
296 | /// 297 | /// Create a [`Parser`](#Parser) that always fails regardless of the input. This 298 | /// uses the `Custom` error constructor of the [`Error`](#Error) type. If you'd 299 | /// like to return a more specific error, you can look at [`fail_with`](#fail_with) 300 | /// instead. 301 | /// 302 | ///
303 | /// Example: 304 | /// 305 | /// import gleam/result.{Ok, Error} 306 | /// import gleam/should 307 | /// import gleam/string 308 | /// import string/parser.{Custom} 309 | /// 310 | /// pub fn example () { 311 | /// let is_not_space = fn (c) { c != " " } 312 | /// let parse_four_letter_word = fn (s) { 313 | /// case string.length(s) == 4 { 314 | /// True -> 315 | /// parser.succeed(s) 316 | /// 317 | /// False -> 318 | /// parser.fail("Expected a four letter word.") 319 | /// } 320 | /// } 321 | /// let parser = parser.take_while(is_not_space) 322 | /// |> parser.then(parse_four_letter_word) 323 | /// 324 | /// parser.run("Hello world", parser) 325 | /// |> should.equal(Error(Custom("Expected a four letter word."))) 326 | /// } 327 | ///
328 | /// 329 | ///
330 | /// 331 | /// Back to top ↑ 332 | /// 333 | ///
334 | /// 335 | pub fn fail(message: String) -> Parser(a) { 336 | Parser(fn(_) { Error(Custom(message)) }) 337 | } 338 | 339 | ///
340 | /// 341 | /// Spot a typo? Open an issue! 342 | /// 343 | ///
344 | /// 345 | /// Create a [`Parser`](#Parser) that always fails regardless of the input. Unlike 346 | /// [`fail`](#fail), you can use any of the constructors for the [`Error`](#Error) 347 | /// type here. 348 | /// 349 | ///
350 | /// Example: 351 | /// 352 | /// import gleam/result.{Ok, Error} 353 | /// import gleam/should 354 | /// import gleam/string 355 | /// import string/parser.{Expected} 356 | /// 357 | /// pub fn example () { 358 | /// let is_not_space = fn (c) { c != " " } 359 | /// let parse_four_letter_word = fn (s) { 360 | /// case string.length(s) == 4 { 361 | /// True -> 362 | /// parser.succeed(s) 363 | /// 364 | /// False -> 365 | /// parser.fail_with(Expected("A four letter word", got: s)) 366 | /// } 367 | /// } 368 | /// let parser = parser.take_while(is_not_space) 369 | /// |> parser.then(parse_four_letter_word) 370 | /// 371 | /// parser.run("Hello world", parser) 372 | /// |> should.equal(Error(Expected("A four letter word", got: s)) 373 | /// } 374 | ///
375 | /// 376 | ///
377 | /// 378 | /// Back to top ↑ 379 | /// 380 | ///
381 | /// 382 | pub fn fail_with(error: Error) -> Parser(a) { 383 | Parser(fn(_) { Error(error) }) 384 | } 385 | 386 | // PRIMITIVE PARSERS ----------------------------------------------------------- 387 | ///
388 | /// 389 | /// Spot a typo? Open an issue! 390 | /// 391 | ///
392 | /// 393 | /// Parse a single grapheme from the input, it could be anything! If the input 394 | /// is an empty string, this will _fail_ with the `EOF` error. 395 | /// 396 | ///
397 | /// Example: 398 | /// 399 | /// import gleam/result.{Ok, Error} 400 | /// import gleam/should 401 | /// import string/parser 402 | /// 403 | /// pub fn example () { 404 | /// let parser = parser.any() 405 | /// 406 | /// parser.run("Hello world", parser) 407 | /// |> should.equal(Ok("H")) 408 | /// } 409 | ///
410 | /// 411 | ///
412 | /// 413 | /// Back to top ↑ 414 | /// 415 | ///
416 | /// 417 | pub fn any() -> Parser(String) { 418 | Parser(fn(input) { 419 | string.pop_grapheme(input) 420 | |> result.replace_error(EOF) 421 | }) 422 | } 423 | 424 | ///
425 | /// 426 | /// Spot a typo? Open an issue! 427 | /// 428 | ///
429 | /// 430 | /// This parser only succeeds when the input is an empty string. Why is that 431 | /// useful? You can use this in combination with your other parsers to ensure 432 | /// that you've consumed **all** the input. 433 | /// 434 | ///
435 | /// Example: 436 | /// 437 | /// import gleam/function 438 | /// import gleam/result.{Ok, Error} 439 | /// import gleam/should 440 | /// import string/parser.{Expected} 441 | /// 442 | /// pub fn example () { 443 | /// let parser = parser.succeed(function.identity) 444 | /// |> parser.keep(parser.string("Hello")) 445 | /// |> parser.drop(parser.eof()) 446 | /// 447 | /// parser.run("Hello", parser) 448 | /// |> should.equal(Ok("Hello")) 449 | /// 450 | /// parser.run("Hello world", parser) 451 | /// |> should.equal(Error(Expected("End of file", got: " world"))) 452 | /// } 453 | ///
454 | /// 455 | ///
456 | /// 457 | /// Back to top ↑ 458 | /// 459 | ///
460 | /// 461 | pub fn eof() -> Parser(Nil) { 462 | Parser(fn(input) { 463 | case string.is_empty(input) { 464 | True -> Ok(#(Nil, input)) 465 | 466 | False -> Error(Expected("End of file", got: input)) 467 | } 468 | }) 469 | } 470 | 471 | ///
472 | /// 473 | /// Spot a typo? Open an issue! 474 | /// 475 | ///
476 | /// 477 | /// Parse an exact string from the input. If you were writing a programming 478 | /// language parser, you might use this to parse keywords or symbols. 479 | /// 480 | ///
481 | /// Example: 482 | /// 483 | /// import gleam/result.{Ok, Error} 484 | /// import gleam/should 485 | /// import gleam/string.{String} 486 | /// import string/parser.{Expected} 487 | /// 488 | /// type VariableDeclaration { 489 | /// VariableDeclaration(var: String) 490 | /// } 491 | /// 492 | /// pub fn example () { 493 | /// let is_not_space = fn (c) { c != " " } 494 | /// let parser = parser.succeed(VariableDeclaration) 495 | /// |> parser.drop(parser.string("var")) 496 | /// |> parser.drop(parser.spaces()) 497 | /// |> parser.keep(parser.take_while(is_not_space)) 498 | /// |> parser.drop(parser.string(";")) 499 | /// 500 | /// parser.run("var x;", parser) 501 | /// |> should.equal(Ok(VariableDeclaration(var: "x"))) 502 | /// 503 | /// parser.run("let x;", parser) 504 | /// |> should.equal(Error(Expected("var", got: "let x;"))) 505 | /// } 506 | ///
507 | /// 508 | ///
509 | /// 510 | /// Back to top ↑ 511 | /// 512 | ///
513 | /// 514 | pub fn string(value: String) -> Parser(String) { 515 | let expect = string.concat(["A string that starts with '", value, "'"]) 516 | 517 | Parser(fn(input) { 518 | case string.split_once(input, value) { 519 | Ok(#("", rest)) -> Ok(#(input, rest)) 520 | _ -> Error(Expected(expect, got: input)) 521 | } 522 | }) 523 | } 524 | 525 | ///
526 | /// 527 | /// Spot a typo? Open an issue! 528 | /// 529 | ///
530 | /// 531 | /// Parse **zero or more** spaces in sequence. 532 | /// 533 | ///
534 | /// Example: 535 | /// 536 | /// import gleam/result.{Ok, Error} 537 | /// import gleam/should 538 | /// import string/parser.{Expected} 539 | /// 540 | /// pub fn example () { 541 | /// let parser = parser.succeed("No spaces required") 542 | /// |> parser.drop(parser.string("Hello")) 543 | /// |> parser.drop(parser.spaces()) 544 | /// |> parser.drop(parser.string("world")) 545 | /// 546 | /// parser.run("Helloworld", parser) 547 | /// |> should.equal(Ok("No spaces required")) 548 | /// 549 | /// let is_space = fn (c) { c == " " } 550 | /// let parser = parser.succeed("At least one space required") 551 | /// |> parser.drop(parser.string("Hello")) 552 | /// |> parser.drop(parser.take_if(is_space)) 553 | /// |> parser.drop(parser.spaces()) 554 | /// |> parser.drop(parser.string("world")) 555 | /// 556 | /// parser.run("Helloworld", parser) 557 | /// |> should.equal(Error(Expected(" ", got: "world"))) 558 | /// 559 | /// parser.run("Hello world", parser) 560 | /// |> should.equal(Ok("At least one space required")) 561 | /// } 562 | ///
563 | /// 564 | ///
565 | /// 566 | /// Back to top ↑ 567 | /// 568 | ///
569 | /// 570 | pub fn spaces() -> Parser(Nil) { 571 | take_while(fn(c) { c == " " }) 572 | |> map(fn(_) { Nil }) 573 | } 574 | 575 | ///
576 | /// 577 | /// Spot a typo? Open an issue! 578 | /// 579 | ///
580 | /// 581 | /// Parse **zero or more** whitespace characters in sequence. Unlike [`spaces`](#spaces), 582 | /// this parser also consumes newlines and tabs as well. 583 | /// 584 | ///
585 | /// Example: 586 | /// 587 | /// import gleam/result.{Nil, Ok, Error} 588 | /// import gleam/should 589 | /// import string/parser 590 | /// 591 | /// pub fn example () { 592 | /// let parser = parser.succeed(Nil) 593 | /// |> parser.drop(parser.string("Hello")) 594 | /// |> parser.drop(parser.whitespace()) 595 | /// |> parser.drop(parser.string("world")) 596 | /// 597 | /// let input = "hello 598 | /// world" 599 | /// 600 | /// parser.run(input, parser) 601 | /// |> should.equal(Ok(Nil)) 602 | /// } 603 | ///
604 | /// 605 | ///
606 | /// 607 | /// Back to top ↑ 608 | /// 609 | ///
610 | /// 611 | pub fn whitespace() -> Parser(Nil) { 612 | take_while(fn(c) { c == " " || c == "\t" || c == "\n" }) 613 | |> map(fn(_) { Nil }) 614 | } 615 | 616 | ///
617 | /// 618 | /// Spot a typo? Open an issue! 619 | /// 620 | ///
621 | /// 622 | /// A simple integer parser. Under the hood it uses 623 | /// [`gleam/int.parse`](https://hexdocs.pm/gleam_stdlib/gleam/int/#parse) but 624 | /// it will only parse simple ints, no octals or hexadecimals and no scientific 625 | /// notation either. 626 | /// 627 | /// If you need something yourself you can always build it using the combinators 628 | /// here, and [pull requests are always welcome](https://github.com/pd-andy/gleam-string-parser/pulls). 629 | /// 630 | ///
631 | /// Example: 632 | /// 633 | /// import gleam/result.{Ok, Error} 634 | /// import gleam/should 635 | /// import string/parser.{Expected} 636 | /// 637 | /// pub fn example () { 638 | /// let parser = parser.int() 639 | /// |> parser.map(fn (x) { x * 2 }) 640 | /// 641 | /// parser.run("25", parser) 642 | /// |> should.equal(Ok(50)) 643 | /// } 644 | ///
645 | /// 646 | ///
647 | /// 648 | /// Back to top ↑ 649 | /// 650 | ///
651 | /// 652 | pub fn int() -> Parser(Int) { 653 | let is_digit = fn(c) { 654 | case c { 655 | "0" | "1" | "2" | "3" | "4" -> True 656 | "5" | "6" | "7" | "8" | "9" -> True 657 | _ -> False 658 | } 659 | } 660 | 661 | take_if_and_while(is_digit) 662 | |> map(int.parse) 663 | |> then(from_result) 664 | } 665 | 666 | ///
667 | /// 668 | /// Spot a typo? Open an issue! 669 | /// 670 | ///
671 | /// 672 | /// 673 | /// 674 | ///
675 | /// Example: 676 | /// 677 | /// import gleam/result.{Ok, Error} 678 | /// import gleam/should 679 | /// import string/parser.{Expected} 680 | /// 681 | /// pub fn example () { 682 | /// 683 | /// } 684 | ///
685 | /// 686 | ///
687 | /// 688 | /// Back to top ↑ 689 | /// 690 | ///
691 | /// 692 | pub fn float() -> Parser(Float) { 693 | let is_digit = fn(c) { 694 | case c { 695 | "0" | "1" | "2" | "3" | "4" -> True 696 | "5" | "6" | "7" | "8" | "9" -> True 697 | _ -> False 698 | } 699 | } 700 | 701 | succeed2(fn(x, y) { string.concat([x, ".", y]) }) 702 | |> keep(take_if_and_while(is_digit)) 703 | |> drop(string(".")) 704 | |> keep(take_if_and_while(is_digit)) 705 | |> map(float.parse) 706 | |> then(from_result) 707 | } 708 | 709 | ///
710 | /// 711 | /// Spot a typo? Open an issue! 712 | /// 713 | ///
714 | /// 715 | /// Take a [`Parser`](#Parser) and turn it into an optional parser. If it fails, 716 | /// instead of the parser reporting an error, we carry on and succeed with `None`. 717 | /// 718 | ///
719 | /// Example: 720 | /// 721 | /// import gleam/result.{Ok, Error} 722 | /// import gleam/should 723 | /// import string/parser.{Expected} 724 | /// 725 | /// pub fn example () { 726 | /// 727 | /// } 728 | ///
729 | /// 730 | ///
731 | /// 732 | /// Back to top ↑ 733 | /// 734 | ///
735 | /// 736 | pub fn optional(parser: Parser(a)) -> Parser(Option(a)) { 737 | Parser(fn(input) { 738 | runwrap(parser, input) 739 | |> result.map(pair.map_first(_, Some)) 740 | |> result.unwrap(#(None, input)) 741 | |> Ok 742 | }) 743 | } 744 | 745 | // UNWRAPPING OTHER TYPES ------------------------------------------------------ 746 | ///
747 | /// 748 | /// Spot a typo? Open an issue! 749 | /// 750 | ///
751 | /// 752 | /// 753 | /// 754 | ///
755 | /// Example: 756 | /// 757 | /// import gleam/result.{Ok, Error} 758 | /// import gleam/should 759 | /// import string/parser.{Expected} 760 | /// 761 | /// pub fn example () { 762 | /// 763 | /// } 764 | ///
765 | /// 766 | ///
767 | /// 768 | /// Back to top ↑ 769 | /// 770 | ///
771 | /// 772 | pub fn from_option(value: Option(a)) -> Parser(a) { 773 | option.map(value, succeed) 774 | // TODO: This needs to be much nicer. 775 | |> option.unwrap(fail_with(UnexpectedInput(""))) 776 | } 777 | 778 | ///
779 | /// 780 | /// Spot a typo? Open an issue! 781 | /// 782 | ///
783 | /// 784 | /// 785 | /// 786 | ///
787 | /// Example: 788 | /// 789 | /// import gleam/result.{Ok, Error} 790 | /// import gleam/should 791 | /// import string/parser.{Expected} 792 | /// 793 | /// pub fn example () { 794 | /// 795 | /// } 796 | ///
797 | /// 798 | ///
799 | /// 800 | /// Back to top ↑ 801 | /// 802 | ///
803 | /// 804 | pub fn from_result(value: Result(a, x)) -> Parser(a) { 805 | result.map(value, succeed) 806 | // TODO: This needs to be much nicer. 807 | |> result.unwrap(fail_with(UnexpectedInput(""))) 808 | } 809 | 810 | // PREDICATE PARSERS ----------------------------------------------------------- 811 | ///
812 | /// 813 | /// Spot a typo? Open an issue! 814 | /// 815 | ///
816 | /// 817 | /// Pop a grapheme off the input string and test it against some predicate function. 818 | /// If it passes, pop the next grapheme off and so on until the predicate test 819 | /// fails. Join all the passing graphemes back together into a single string and 820 | /// succeed with that result. 821 | /// 822 | /// **This parser always succeeds**. If you use `[`take_while`](#take_while) to 823 | /// parse an empty string, or if no graphemes pass the predicate, this will succeed 824 | /// with an empty string of its own. 825 | /// 826 | ///
827 | /// Example: 828 | /// 829 | /// import gleam/result.{Ok, Error} 830 | /// import gleam/should 831 | /// import string/parser 832 | /// 833 | /// pub fn example () { 834 | /// let is_digit = fn (c) { 835 | /// case c { 836 | /// "0" | "1" | "2" | "3" | "4" -> True 837 | /// "5" | "6" | "7" | "8" | "9" -> True 838 | /// _ -> False 839 | /// } 840 | /// } 841 | /// let parser = parser.take_while(is_digit) 842 | /// 843 | /// parser.run("1337", parser) 844 | /// |> should.equal(Ok("1337")) 845 | /// 846 | /// parser.run("Hello world", parser) 847 | /// |> should.equal(Ok("")) 848 | /// 849 | /// parser.run("", parser) 850 | /// |> should.equal(Ok("")) 851 | /// } 852 | ///
853 | /// 854 | ///
855 | /// 856 | /// Back to top ↑ 857 | /// 858 | ///
859 | /// 860 | pub fn take_while(predicate: fn(String) -> Bool) -> Parser(String) { 861 | let recurse = fn(c) { 862 | take_while(predicate) 863 | |> map(string.append(c, _)) 864 | } 865 | 866 | Parser(fn(input) { 867 | case string.pop_grapheme(input) { 868 | Ok(#(char, rest)) -> 869 | case predicate(char) { 870 | True -> runwrap(recurse(char), rest) 871 | False -> Ok(#("", input)) 872 | } 873 | 874 | Error(Nil) -> Ok(#("", "")) 875 | } 876 | }) 877 | } 878 | 879 | ///
880 | /// 881 | /// Spot a typo? Open an issue! 882 | /// 883 | ///
884 | /// 885 | /// Pop a grapheme off the input and test it against some predicate function. If 886 | /// it passes then succeed with that grapheme, otherwise **fail** with the 887 | /// `UnexpectedInput` constructor of the [`Error`](#Error) type. This is in 888 | /// contrast to [`take_while`](#take_while) which will always succeed even if no 889 | /// graphemes pass the predicate. 890 | /// 891 | ///
892 | /// Example: 893 | /// 894 | /// import gleam/result.{Ok, Error} 895 | /// import gleam/should 896 | /// import string/parser.{UnexpectedInput, EOF} 897 | /// 898 | /// pub fn example () { 899 | /// let is_digit = fn (c) { 900 | /// case c { 901 | /// "0" | "1" | "2" | "3" | "4" -> True 902 | /// "5" | "6" | "7" | "8" | "9" -> True 903 | /// _ -> False 904 | /// } 905 | /// } 906 | /// let parser = parser.take_while(is_digit) 907 | /// 908 | /// parser.run("1337", parser) 909 | /// |> should.equal(Ok("1")) 910 | /// 911 | /// parser.run("Hello world", parser) 912 | /// |> should.equal(Error(UnexpectedInput)) 913 | /// 914 | /// parser.run("", parser) 915 | /// |> should.equal(Error(EOF)) 916 | /// } 917 | ///
918 | /// 919 | ///
920 | /// 921 | /// Back to top ↑ 922 | /// 923 | ///
924 | /// 925 | pub fn take_if(predicate: fn(String) -> Bool) -> Parser(String) { 926 | Parser(fn(input) { 927 | case string.pop_grapheme(input) { 928 | Ok(#(char, rest)) -> 929 | case predicate(char) { 930 | True -> Ok(#(char, rest)) 931 | False -> Error(UnexpectedInput(input)) 932 | } 933 | 934 | Error(Nil) -> Error(EOF) 935 | } 936 | }) 937 | } 938 | 939 | ///
940 | /// 941 | /// Spot a typo? Open an issue! 942 | /// 943 | ///
944 | /// 945 | /// It's incredibly common to combine [`take_if`](#take_if) and [`take_while`](#take_while) 946 | /// to create a parser that consumes _one or more_ graphemes. This does just that! 947 | /// 948 | ///
949 | /// Example: 950 | /// 951 | /// import gleam/result.{Ok, Error} 952 | /// import gleam/should 953 | /// import string/parser.{UnexpectedInput, EOF} 954 | /// 955 | /// pub fn example () { 956 | /// let is_digit = fn (c) { 957 | /// case c { 958 | /// "0" | "1" | "2" | "3" | "4" -> True 959 | /// "5" | "6" | "7" | "8" | "9" -> True 960 | /// _ -> False 961 | /// } 962 | /// } 963 | /// let parser = parser.take_if_and_while(is_digit) 964 | /// 965 | /// parser.run("1337", parser) 966 | /// |> should.equal(Ok("1337")) 967 | /// 968 | /// parser.run("1", parser) 969 | /// |> should.equal(Ok("1")) 970 | /// 971 | /// parser.run("Hello world", parser) 972 | /// |> should.equal(Error(UnexpectedInput)) 973 | /// 974 | /// parser.run("", parser) 975 | /// |> should.equal(Error(EOF)) 976 | /// } 977 | ///
978 | /// 979 | ///
980 | /// 981 | /// Back to top ↑ 982 | /// 983 | ///
984 | /// 985 | pub fn take_if_and_while(predicate: fn(String) -> Bool) -> Parser(String) { 986 | succeed2(string.append) 987 | |> keep(take_if(predicate)) 988 | |> keep(take_while(predicate)) 989 | } 990 | 991 | // COMBINATORS ----------------------------------------------------------------- 992 | ///
993 | /// 994 | /// Spot a typo? Open an issue! 995 | /// 996 | ///
997 | /// 998 | /// A combinator that can take the result of one parser, and use it to create 999 | /// a new parser. This follows the same pattern as 1000 | /// [`gleam/option.then`](https://hexdocs.pm/gleam_stdlib/gleam/option/#then) or 1001 | /// [`gleam/result.then`](https://hexdocs.pm/gleam_stdlib/gleam/option/#then). 1002 | /// 1003 | /// This is useful if you want to transform or validate a parsed value and fail 1004 | /// if something is wrong. 1005 | /// 1006 | ///
1007 | /// import gleam/int 1008 | /// import gleam/result.{Ok, Error} 1009 | /// import gleam/should 1010 | /// import string/parser.{UnexpectedInput} 1011 | /// 1012 | /// pub fn example () { 1013 | /// let is_digit = fn (c) { 1014 | /// case c { 1015 | /// "0" | "1" | "2" | "3" | "4" -> True 1016 | /// "5" | "6" | "7" | "8" | "9" -> True 1017 | /// _ -> False 1018 | /// } 1019 | /// } 1020 | /// let parse_digits = fn (digits) { 1021 | /// case int.parse(digits) { 1022 | /// Ok(num) -> 1023 | /// parser.succeed(num) 1024 | /// 1025 | /// Error(_) -> 1026 | /// parser.fail_with(UnexpectedInput) 1027 | /// } 1028 | /// 1029 | /// let parser = parser.take_while(is_digit) 1030 | /// |> parser.then(parse_digits) 1031 | /// 1032 | /// parser.run("1337", parser) 1033 | /// |> should.equal(Ok(1337)) 1034 | /// 1035 | /// parser.run("Hello world", parser) 1036 | /// |> should.equal(Error(UnexpectedInput)) 1037 | /// } 1038 | ///
1039 | /// 1040 | ///
1041 | /// 1042 | /// Back to top ↑ 1043 | /// 1044 | ///
1045 | /// 1046 | pub fn then(parser: Parser(a), f: fn(a) -> Parser(b)) -> Parser(b) { 1047 | Parser(fn(input) { 1048 | runwrap(parser, input) 1049 | |> result.then(fn(result) { 1050 | let #(value, next_input) = result 1051 | 1052 | runwrap(f(value), next_input) 1053 | }) 1054 | }) 1055 | } 1056 | 1057 | ///
1058 | /// 1059 | /// Spot a typo? Open an issue! 1060 | /// 1061 | ///
1062 | /// 1063 | /// A combinator that transforms a parsed value by applying a function to it and 1064 | /// returning a new [`Parser`](#Parser) with that transformed value. This follows 1065 | /// the same pattern as [`gleam/option.map`](https://hexdocs.pm/gleam_stdlib/gleam/option/#map) 1066 | /// or [`gleam/result.map`](https://hexdocs.pm/gleam_stdlib/gleam/result/#map). 1067 | /// 1068 | ///
1069 | /// import gleam/result.{Ok, Error} 1070 | /// import gleam/should 1071 | /// import string/parser 1072 | /// 1073 | /// pub fn example () { 1074 | /// let parser = parser.any() |> parser.map(string.repeat(5)) 1075 | /// 1076 | /// parser.run("Hello world", parser) 1077 | /// |> should.equal(Ok("HHHHH")) 1078 | /// 1079 | /// parser.run("", parser) 1080 | /// |> should.equal(Error(parser.EOF)) 1081 | /// } 1082 | ///
1083 | /// 1084 | ///
1085 | /// 1086 | /// Back to top ↑ 1087 | /// 1088 | ///
1089 | /// 1090 | pub fn map(parser: Parser(a), f: fn(a) -> b) -> Parser(b) { 1091 | then( 1092 | parser, 1093 | fn(a) { 1094 | f(a) 1095 | |> succeed 1096 | }, 1097 | ) 1098 | } 1099 | 1100 | ///
1101 | /// 1102 | /// Spot a typo? Open an issue! 1103 | /// 1104 | ///
1105 | /// 1106 | /// 1107 | /// 1108 | ///
1109 | /// import gleam/result.{Ok, Error} 1110 | /// import gleam/should 1111 | /// import string/parser 1112 | /// 1113 | /// pub fn example () { 1114 | /// 1115 | /// } 1116 | ///
1117 | /// 1118 | ///
1119 | /// 1120 | /// Back to top ↑ 1121 | /// 1122 | ///
1123 | /// 1124 | pub fn lazy(parser: fn() -> Parser(a)) -> Parser(a) { 1125 | Parser(fn(input) { runwrap(parser(), input) }) 1126 | } 1127 | 1128 | ///
1129 | /// 1130 | /// Spot a typo? Open an issue! 1131 | /// 1132 | ///
1133 | /// 1134 | /// A combinator that combines two parsed values by applying a function to them 1135 | /// both and returning a new [`Parser`](#Parser) containing the combined value. 1136 | /// 1137 | /// Fun fact, [`keep`](#keep) is defined using this combinator. 1138 | /// 1139 | ///
1140 | /// Example: 1141 | /// 1142 | /// import gleam/result.{Ok, Error} 1143 | /// import gleam/should 1144 | /// import gleam/string 1145 | /// import string/parser 1146 | /// 1147 | /// pub fn example () { 1148 | /// let parser = parser.map2( 1149 | /// parser.string("Hello"), 1150 | /// parser.string("world"), 1151 | /// fn (hello, world) { 1152 | /// string.concat([ hello, " ", world ]) 1153 | /// } 1154 | /// ) 1155 | /// 1156 | /// parser.run("Helloworld", parser) 1157 | /// |> should.equal(Ok("Hello world")) 1158 | /// } 1159 | ///
1160 | /// 1161 | ///
1162 | /// 1163 | /// Back to top ↑ 1164 | /// 1165 | ///
1166 | /// 1167 | pub fn map2( 1168 | parser_a: Parser(a), 1169 | parser_b: Parser(b), 1170 | f: fn(a, b) -> c, 1171 | ) -> Parser(c) { 1172 | then(parser_a, fn(a) { map(parser_b, fn(b) { f(a, b) }) }) 1173 | } 1174 | 1175 | ///
1176 | /// 1177 | /// Spot a typo? Open an issue! 1178 | /// 1179 | ///
1180 | /// 1181 | /// A combinator that tries a list of parsers in sequence until one succeeds. If 1182 | /// you pass in an empty list this parser will fail with the `BadParser` constructor 1183 | /// of the [`Error`](#Error) type. 1184 | /// 1185 | ///
1186 | /// import gleam/result.{Ok, Error} 1187 | /// import gleam/should 1188 | /// import gleam/string 1189 | /// import string/parser 1190 | /// 1191 | /// pub fn example () { 1192 | /// let beam_lang = parser.oneOf( 1193 | /// parser.string("Erlang"), 1194 | /// parser.string("Elixir"), 1195 | /// parser.string("Gleam") 1196 | /// ) 1197 | /// let parser = parser.succeed2(string.append) 1198 | /// |> parser.keep(parser.string("Hello ")) 1199 | /// |> parser.keep(beam_lang) 1200 | /// 1201 | /// parser.run("Hello Gleam", parser) 1202 | /// |> should.equal(Ok("Hello Gleam")) 1203 | /// } 1204 | ///
1205 | /// 1206 | ///
1207 | /// 1208 | /// Back to top ↑ 1209 | /// 1210 | ///
1211 | /// 1212 | pub fn one_of(parsers: List(Parser(a))) -> Parser(a) { 1213 | let initial_error = 1214 | Error(BadParser( 1215 | "The list of parsers supplied to one_of is empty, I will always fail!", 1216 | )) 1217 | 1218 | Parser(fn(input) { 1219 | list.fold_until( 1220 | over: parsers, 1221 | from: initial_error, 1222 | with: fn(_, parser) { 1223 | let result = runwrap(parser, input) 1224 | 1225 | case result.is_ok(result) { 1226 | True -> Stop(result) 1227 | 1228 | False -> Continue(result) 1229 | } 1230 | }, 1231 | ) 1232 | }) 1233 | } 1234 | 1235 | ///
1236 | /// 1237 | /// Spot a typo? Open an issue! 1238 | /// 1239 | ///
1240 | /// 1241 | /// 1242 | /// 1243 | ///
1244 | /// import gleam/result.{Ok, Error} 1245 | /// import gleam/should 1246 | /// import gleam/string 1247 | /// import string/parser 1248 | /// 1249 | /// pub fn example () { 1250 | /// 1251 | /// } 1252 | ///
1253 | /// 1254 | ///
1255 | /// 1256 | /// Back to top ↑ 1257 | /// 1258 | ///
1259 | /// 1260 | pub fn many(parser: Parser(a), separator: Parser(b)) -> Parser(List(a)) { 1261 | let recurse = fn(value) { 1262 | many(parser, separator) 1263 | |> map(fn(vals) { [value, ..vals] }) 1264 | } 1265 | 1266 | Parser(fn(input) { 1267 | case runwrap( 1268 | parser 1269 | |> drop(separator), 1270 | input, 1271 | ) { 1272 | Ok(#(value, rest)) -> runwrap(recurse(value), rest) 1273 | 1274 | Error(_) -> 1275 | case runwrap(parser, input) { 1276 | Ok(#(value, rest)) -> Ok(#([value], rest)) 1277 | Error(_) -> Ok(#([], input)) 1278 | } 1279 | } 1280 | }) 1281 | } 1282 | 1283 | // CHAINING PARSERS ------------------------------------------------------------ 1284 | ///
1285 | /// 1286 | /// Spot a typo? Open an issue! 1287 | /// 1288 | ///
1289 | /// 1290 | /// This is one of two combinators used in conjuction with [`succeed`](#succeed) 1291 | /// (the other being [`drop`](#drop)) that come together to form a nice pipeline 1292 | /// API for parsing. 1293 | /// 1294 | /// The first argument is a _function_ wrapped up in a [`Parser`](#Parser), usually 1295 | /// created using [`succeed`](#succeed). The second argument is a parser for the 1296 | /// value we want to keep. This parser combines the two by applying that value 1297 | /// to the function. 1298 | /// 1299 | /// When you use one of the `succeed{N}` functions such as [`succeed2`](#succeed) 1300 | /// you'll get back a [`Parser`] for a function. You might then work out that you 1301 | /// can use `keep` again, and voila we have a neat declarative pipline parser 1302 | /// that describes what results we want to keep and how. 1303 | /// 1304 | /// The explanation is a bit wordy and compicated, but its useage is intuitive. 1305 | /// If you've been looking at the rest of the docs for this package, you'll already 1306 | /// have seen [`keep`](#keep) used all over the place. 1307 | /// 1308 | ///
1309 | /// Example: 1310 | /// 1311 | /// import gleam/result.{Ok, Error} 1312 | /// import gleam/should 1313 | /// import gleam/string.{String} 1314 | /// import gleam/int 1315 | /// import string/parser.{UnexpectedInput} 1316 | /// 1317 | /// pub fn example () { 1318 | /// let digit_parser = parser.any() |> parser.then(fn (c) { 1319 | /// case int.parse(c) { 1320 | /// Ok(num) -> parser.succeed(num) 1321 | /// Error(_) -> parser.fail_with(UnexpectedInput) 1322 | /// } 1323 | /// }) 1324 | /// 1325 | /// let parser = parser.succeed2(fn (x, y) { x + y }) 1326 | /// |> parser.keep(digit_parser) 1327 | /// |> parser.drop(parser.spaces()) 1328 | /// |> parser.drop(parser.string("+")) 1329 | /// |> parser.drop(parser.spaces()) 1330 | /// |> parser.keep(digit_parser) 1331 | /// 1332 | /// parser.run("1 + 3", parser) 1333 | /// |> should.equal(Ok(4)) 1334 | /// } 1335 | ///
1336 | /// 1337 | ///
1338 | /// 1339 | /// Back to top ↑ 1340 | /// 1341 | ///
1342 | /// 1343 | pub fn keep(mapper: Parser(fn(a) -> b), parser: Parser(a)) -> Parser(b) { 1344 | map2(mapper, parser, fn(f, a) { f(a) }) 1345 | } 1346 | 1347 | ///
1348 | /// 1349 | /// Spot a typo? Open an issue! 1350 | /// 1351 | ///
1352 | /// 1353 | /// This is one of two combinators used in conjuction with [`succeed`](#succeed) 1354 | /// (the other being [`keep`](#keep)) that come together to form a nice pipeline 1355 | /// API for parsing. 1356 | /// 1357 | /// This combinator runs two parsers in sequence, and then keeps the result of 1358 | /// the first parser while ignoring the result of the second. This allows us to 1359 | /// write parsers that enforce a particular structure from the input without 1360 | /// needing to _do_ anything with the result of those structural parsers. 1361 | /// 1362 | /// Think of parsing JSON, for example. We need to parse opening and closing curly 1363 | /// braces to know it's a valid object, and we need to parse the separating colon 1364 | /// to differentiate between key and value. We need to parse these things, but 1365 | /// they're _structural_, we might want to parse a key/value pair into a Gleam 1366 | /// tuple; we don't care about the curly braces or colons but we need to know 1367 | /// they're there. 1368 | /// 1369 | ///
1370 | /// Example: 1371 | /// 1372 | /// import gleam/result.{Ok, Error} 1373 | /// import gleam/should 1374 | /// import gleam/string.{String} 1375 | /// import gleam/int 1376 | /// import string/parser.{UnexpectedInput} 1377 | /// 1378 | /// pub fn example () { 1379 | /// let digit_parser = parser.any() |> parser.then(fn (c) { 1380 | /// case int.parse(c) { 1381 | /// Ok(num) -> parser.succeed(num) 1382 | /// Error(_) -> parser.fail_with(UnexpectedInput) 1383 | /// } 1384 | /// }) 1385 | /// 1386 | /// let parser = parser.succeed2(fn (x, y) { x + y }) 1387 | /// |> parser.keep(digit_parser) 1388 | /// |> parser.drop(parser.spaces()) 1389 | /// |> parser.drop(parser.string("+")) 1390 | /// |> parser.drop(parser.spaces()) 1391 | /// |> parser.keep(digit_parser) 1392 | /// 1393 | /// parser.run("1 + 3", parser) 1394 | /// |> should.equal(Ok(4)) 1395 | /// } 1396 | ///
1397 | /// 1398 | ///
1399 | /// 1400 | /// Back to top ↑ 1401 | /// 1402 | ///
1403 | /// 1404 | pub fn drop(keeper: Parser(a), ignorer: Parser(b)) -> Parser(a) { 1405 | map2(keeper, ignorer, fn(a, _) { a }) 1406 | } 1407 | -------------------------------------------------------------------------------- /test/currency.gleam: -------------------------------------------------------------------------------- 1 | import gleam/float 2 | import gleam/result 3 | import gleeunit/should 4 | import string/parser.{Parser} 5 | import gleam/list 6 | 7 | // TESTS ----------------------------------------------------------------------- 8 | pub fn currency_test() { 9 | let to_usd = fn(currency: Currency) -> Float { 10 | case currency.code { 11 | GBP -> currency.amount *. 1.38 12 | EUR -> currency.amount *. 1.17 13 | USD -> currency.amount 14 | } 15 | } 16 | let add = fn(a, b) { a +. b } 17 | let input = "$2.30, €3.24, £5.50, $4.13" 18 | 19 | parser.run(input, wallet_parser()) 20 | |> result.map(list.map(_, to_usd)) 21 | |> result.map(list.fold(_, 0.0, add)) 22 | |> should.equal(Ok(17.8108)) 23 | } 24 | 25 | // TYPES ----------------------------------------------------------------------- 26 | type Currency { 27 | Currency(code: CurrencyCode, amount: Float) 28 | } 29 | 30 | type CurrencyCode { 31 | GBP 32 | EUR 33 | USD 34 | } 35 | 36 | type Wallet = 37 | List(Currency) 38 | 39 | // PARSERS --------------------------------------------------------------------- 40 | fn currency_parser() -> Parser(Currency) { 41 | parser.succeed2(Currency) 42 | |> parser.keep(currency_code_parser()) 43 | |> parser.keep(parser.float()) 44 | // Consume any trailing whitespace after parsing the Currency. 45 | |> parser.drop(parser.spaces()) 46 | } 47 | 48 | fn currency_code_parser() -> Parser(CurrencyCode) { 49 | parser.any() 50 | |> parser.then(fn(symbol) { 51 | case symbol { 52 | "£" -> parser.succeed(GBP) 53 | "€" -> parser.succeed(EUR) 54 | "$" -> parser.succeed(USD) 55 | _ -> parser.fail("Unknown currency symbol.") 56 | } 57 | }) 58 | } 59 | 60 | fn wallet_parser() -> Parser(Wallet) { 61 | let separator = 62 | parser.string(",") 63 | |> parser.drop(parser.spaces()) 64 | 65 | parser.many(currency_parser(), separator) 66 | |> parser.drop(parser.eof()) 67 | } 68 | -------------------------------------------------------------------------------- /test/json.gleam: -------------------------------------------------------------------------------- 1 | import gleam/bool 2 | import gleam/float 3 | import gleam/function 4 | import gleam/int 5 | import gleam/list 6 | import gleeunit/should 7 | import gleam/string 8 | import string/parser.{Parser} 9 | 10 | // TESTS ----------------------------------------------------------------------- 11 | pub fn json_test() { 12 | let input = "{ \"foo\": 3.14, \"bar\": [ \"hello\", null ] }" 13 | let ast = 14 | JsonObject([ 15 | #("foo", JsonNumber(3.14)), 16 | #("bar", JsonArray([JsonString("hello"), JsonNull])), 17 | ]) 18 | 19 | parser.run(input, json_parser()) 20 | |> should.equal(Ok(ast)) 21 | } 22 | 23 | // TYPES ----------------------------------------------------------------------- 24 | type JSON { 25 | JsonArray(List(JSON)) 26 | JsonBool(Bool) 27 | JsonNull 28 | JsonNumber(Float) 29 | JsonObject(List(#(String, JSON))) 30 | JsonString(String) 31 | } 32 | 33 | // PARSERS --------------------------------------------------------------------- 34 | fn json_parser() -> Parser(JSON) { 35 | parser.one_of([ 36 | // We need to use `parser.lazy` here because the array parser (and the 37 | // object one too) recursively call `json_parser()`. Without lazy this 38 | // would get stuck in an infinite evaluation loop at runtime! 39 | parser.lazy(fn() { json_array_parser() }), 40 | json_bool_parser(), 41 | json_null_parser(), 42 | json_number_parser(), 43 | parser.lazy(fn() { json_object_parser() }), 44 | json_string_parser(), 45 | ]) 46 | } 47 | 48 | fn json_array_parser() -> Parser(JSON) { 49 | let separator = 50 | parser.spaces() 51 | |> parser.drop(parser.string(",")) 52 | |> parser.drop(parser.spaces()) 53 | 54 | parser.succeed(function.identity) 55 | |> parser.drop(parser.string("[")) 56 | |> parser.drop(parser.spaces()) 57 | |> parser.keep(parser.many(json_parser(), separator)) 58 | |> parser.drop(parser.spaces()) 59 | |> parser.drop(parser.string("]")) 60 | |> parser.map(JsonArray) 61 | } 62 | 63 | fn json_bool_parser() -> Parser(JSON) { 64 | parser.one_of([ 65 | parser.succeed(True) 66 | |> parser.drop(parser.string("true")) 67 | |> parser.map(JsonBool), 68 | parser.succeed(False) 69 | |> parser.drop(parser.string("false")) 70 | |> parser.map(JsonBool), 71 | ]) 72 | } 73 | 74 | fn json_null_parser() -> Parser(JSON) { 75 | parser.succeed(JsonNull) 76 | |> parser.drop(parser.string("null")) 77 | } 78 | 79 | fn json_number_parser() -> Parser(JSON) { 80 | parser.one_of([ 81 | parser.float() 82 | |> parser.map(JsonNumber), 83 | parser.int() 84 | |> parser.map(int.to_float) 85 | |> parser.map(JsonNumber), 86 | ]) 87 | } 88 | 89 | fn json_object_parser() -> Parser(JSON) { 90 | let pair = fn(a, b) { #(a, b) } 91 | 92 | let separator = 93 | parser.spaces() 94 | |> parser.drop(parser.string(",")) 95 | |> parser.drop(parser.spaces()) 96 | 97 | let key_parser = 98 | parser.succeed(function.identity) 99 | |> parser.drop(parser.string("\"")) 100 | |> parser.keep(parser.take_while(fn(c) { c != "\"" })) 101 | |> parser.drop(parser.string("\"")) 102 | 103 | let key_value_pair = 104 | parser.succeed2(pair) 105 | |> parser.keep(key_parser) 106 | |> parser.drop(parser.string(":")) 107 | |> parser.drop(parser.spaces()) 108 | |> parser.keep(json_parser()) 109 | 110 | parser.succeed(function.identity) 111 | |> parser.drop(parser.string("{")) 112 | |> parser.drop(parser.spaces()) 113 | |> parser.keep(parser.many(key_value_pair, separator)) 114 | |> parser.drop(parser.spaces()) 115 | |> parser.drop(parser.string("}")) 116 | |> parser.map(JsonObject) 117 | } 118 | 119 | fn json_string_parser() -> Parser(JSON) { 120 | parser.succeed(function.identity) 121 | |> parser.drop(parser.string("\"")) 122 | |> parser.keep(parser.take_while(fn(c) { c != "\"" })) 123 | |> parser.drop(parser.string("\"")) 124 | |> parser.map(JsonString) 125 | } 126 | -------------------------------------------------------------------------------- /test/string_parser_test.gleam: -------------------------------------------------------------------------------- 1 | import gleeunit 2 | 3 | pub fn main() { 4 | gleeunit.main() 5 | } 6 | --------------------------------------------------------------------------------