├── .gitignore ├── LICENSE ├── README.md ├── elm.json ├── examples ├── Example.elm ├── README.md └── elm.json └── src ├── Elm └── Kernel │ └── Url.js ├── Url.elm └── Url ├── Builder.elm ├── Parser.elm └── Parser ├── Internal.elm └── Query.elm /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Evan Czaplicki 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Work with URLs 2 | 3 | This package helps you (1) build new URLs and (2) parse existing URLs into nice Elm data structures. 4 | 5 | These tasks are quite common when building web apps in Elm with [`Browser.application`](https://package.elm-lang.org/packages/elm/browser/latest/Browser#application)! 6 | 7 | 8 | ## What is a URL? 9 | 10 | A URL is defined by Tim Berners-Lee in [this document](https://tools.ietf.org/html/rfc3986). It is worth reading, but I will try to share some highlights. He shares an example like this: 11 | 12 | ``` 13 | https://example.com:8042/over/there?name=ferret#nose 14 | \___/ \______________/\_________/ \_________/ \__/ 15 | | | | | | 16 | scheme authority path query fragment 17 | ``` 18 | 19 | And here are some facts that I found surprising: 20 | 21 | - **ASCII** — The spec only talks about ASCII characters. Behavior with other encodings is unspecified, so if you use a UTF-8 character directly, it may be handled differently by browsers, packages, and servers! No one is wrong. It is just unspecified. So I would stick to ASCII to be safe. 22 | - **Escaping** — There are some reserved characters in the spec, like `/`, `?`, and `#`. So what happens when you need those in your query? The spec allows you to “escape” characters (`/` => `%2F`, `?` => `%3F`, `#` => `%23`) so it is clearly not a reserved characters anymore. The spec calls this [percent-encoding](https://tools.ietf.org/html/rfc3986#section-2.1). The basic idea is to look up the hex code in [the ASCII table](https://ascii.cl/) and put a `%` in front. There are many subtleties though, so I recommend reading [this](https://en.wikipedia.org/wiki/Percent-encoding) for more details! 23 | 24 | > **Note:** The difference between a URI and a URL is kind of subtle. [This post](https://danielmiessler.com/study/url-uri/) explains the difference nicely. I decided to call this library `elm/url` because it is primarily concerned with HTTP which does need actual locations. 25 | 26 | 27 | ## Related Work 28 | 29 | The API in `Url.Parser` is quite distinctive. I first saw the general idea in Chris Done’s [formatting][] library. Based on that, Noah and I outlined the API you see in `Url.Parser`. Noah then found Rudi Grinberg’s [post][] about type safe routing in OCaml. It was exactly what we were going for. We had even used the names `s` and `()` in our draft API! In the end, we ended up using the “final encoding” of the EDSL that had been left as an exercise for the reader. Very fun to work through! 30 | 31 | [formatting]: https://chrisdone.com/posts/formatting 32 | [post]: http://rgrinberg.com/posts/primitive-type-safe-routing/ 33 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "elm/url", 4 | "summary": "Create and parse URLs. Use for HTTP and \"routing\" in single-page apps (SPAs)", 5 | "license": "BSD-3-Clause", 6 | "version": "1.0.0", 7 | "exposed-modules": [ 8 | "Url", 9 | "Url.Builder", 10 | "Url.Parser", 11 | "Url.Parser.Query" 12 | ], 13 | "elm-version": "0.19.0 <= v < 0.20.0", 14 | "dependencies": { 15 | "elm/core": "1.0.0 <= v < 2.0.0" 16 | }, 17 | "test-dependencies": { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/Example.elm: -------------------------------------------------------------------------------- 1 | module Example exposing (main) 2 | 3 | 4 | import Browser 5 | import Browser.Navigation as Nav 6 | import Html exposing (Html, a, code, div, h1, h3, li, text, ul) 7 | import Html.Attributes exposing (href) 8 | import Url exposing (Url) 9 | import Url.Parser as P exposing (Parser, (), (), s, top) 10 | import Url.Parser.Query as Q 11 | 12 | 13 | 14 | -- MAIN 15 | 16 | 17 | main : Program () Model Msg 18 | main = 19 | Browser.application 20 | { init = init 21 | , view = view 22 | , update = update 23 | , subscriptions = subscriptions 24 | , onUrlRequest = UrlRequest 25 | , onUrlChange = UrlChange 26 | } 27 | 28 | 29 | 30 | -- MODEL 31 | 32 | 33 | type alias Model = 34 | { history : List (Maybe Route) 35 | , key : Nav.Key 36 | } 37 | 38 | 39 | init : () -> Url -> Nav.Key -> ( Model, Cmd Msg ) 40 | init _ url key = 41 | ( Model [ P.parse routeParser url ] key 42 | , Cmd.none 43 | ) 44 | 45 | 46 | 47 | -- URL PARSING 48 | 49 | 50 | type Route 51 | = Home 52 | | BlogList (Maybe String) 53 | | BlogPost Int 54 | 55 | 56 | routeParser : Parser (Route -> a) a 57 | routeParser = 58 | P.oneOf 59 | [ P.map Home top 60 | , P.map BlogList (s "blog" Q.string "search") 61 | , P.map BlogPost (s "blog" P.int) 62 | ] 63 | 64 | 65 | 66 | -- UPDATE 67 | 68 | 69 | type Msg 70 | = UrlChange Url 71 | | UrlRequest Browser.UrlRequest 72 | 73 | 74 | update : Msg -> Model -> ( Model, Cmd Msg ) 75 | update msg model = 76 | case msg of 77 | UrlChange url -> 78 | ( { model | history = P.parse routeParser url :: model.history } 79 | , Cmd.none 80 | ) 81 | 82 | UrlRequest request -> 83 | case request of 84 | Browser.Internal url -> 85 | ( model 86 | , Nav.pushUrl model.key (Url.toString url) 87 | ) 88 | 89 | Browser.External url -> 90 | ( model 91 | , Nav.load url 92 | ) 93 | 94 | 95 | 96 | -- SUBSCRIPTIONS 97 | 98 | 99 | subscriptions : Model -> Sub Msg 100 | subscriptions model = 101 | Sub.none 102 | 103 | 104 | 105 | -- VIEW 106 | 107 | 108 | view : Model -> Browser.Document Msg 109 | view model = 110 | Browser.Document "Example Page for elm/url" 111 | [ div [] 112 | [ h1 [] [ text "Links" ] 113 | , ul [] (List.map viewLink [ "/", "/blog/", "/blog/42", "/blog/37", "/blog/?search=cats" ]) 114 | , h1 [] [ text "History" ] 115 | , ul [] (List.map viewRoute model.history) 116 | ] 117 | ] 118 | 119 | 120 | viewLink : String -> Html Msg 121 | viewLink url = 122 | li [] [ a [ href url ] [ text url ] ] 123 | 124 | 125 | viewRoute : Maybe Route -> Html msg 126 | viewRoute maybeRoute = 127 | case maybeRoute of 128 | Nothing -> 129 | li [] [ code [] [ text "Uknown URL" ] ] 130 | 131 | Just route -> 132 | li [] [ code [] [ text (Debug.toString route) ] ] 133 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Run the Examples 2 | 3 | To run the examples in this folder, follow the following steps: 4 | 5 | ```bash 6 | git clone https://github.com/elm/url 7 | cd url 8 | cd examples 9 | elm reactor 10 | ``` 11 | 12 | This will navigate into the `examples/` directory and start `elm reactor`. From here, go to [http://localhost:8000](http://localhost:8000) and start clicking on `.elm` files to see them in action. 13 | -------------------------------------------------------------------------------- /examples/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "." 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/browser": "1.0.0", 10 | "elm/core": "1.0.0", 11 | "elm/html": "1.0.0", 12 | "elm/http": "1.0.0", 13 | "elm/json": "1.0.0", 14 | "elm/url": "1.0.0" 15 | }, 16 | "indirect": { 17 | "elm/time": "1.0.0", 18 | "elm/virtual-dom": "1.0.2" 19 | } 20 | }, 21 | "test-dependencies": { 22 | "direct": {}, 23 | "indirect": {} 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Elm/Kernel/Url.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | import Maybe exposing (Just, Nothing) 4 | 5 | */ 6 | 7 | function _Url_percentEncode(string) 8 | { 9 | return encodeURIComponent(string); 10 | } 11 | 12 | function _Url_percentDecode(string) 13 | { 14 | try 15 | { 16 | return __Maybe_Just(decodeURIComponent(string)); 17 | } 18 | catch (e) 19 | { 20 | return __Maybe_Nothing; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Url.elm: -------------------------------------------------------------------------------- 1 | module Url exposing 2 | ( Url 3 | , Protocol(..) 4 | , toString 5 | , fromString 6 | , percentEncode 7 | , percentDecode 8 | ) 9 | 10 | 11 | {-| 12 | 13 | # URLs 14 | @docs Url, Protocol, toString, fromString 15 | 16 | # Percent-Encoding 17 | @docs percentEncode, percentDecode 18 | 19 | -} 20 | 21 | 22 | import Elm.Kernel.Url 23 | 24 | 25 | 26 | -- URL 27 | 28 | 29 | {-| In [the URI spec](https://tools.ietf.org/html/rfc3986), Tim Berners-Lee 30 | says a URL looks like this: 31 | 32 | ``` 33 | https://example.com:8042/over/there?name=ferret#nose 34 | \___/ \______________/\_________/ \_________/ \__/ 35 | | | | | | 36 | scheme authority path query fragment 37 | ``` 38 | 39 | When you are creating a single-page app with [`Browser.application`][app], you 40 | use the [`Url.Parser`](Url-Parser) module to turn a `Url` into even nicer data. 41 | 42 | If you want to create your own URLs, check out the [`Url.Builder`](Url-Builder) 43 | module as well! 44 | 45 | [app]: /packages/elm/browser/latest/Browser#application 46 | 47 | **Note:** This is a subset of all the full possibilities listed in the URI 48 | spec. Specifically, it does not accept the `userinfo` segment you see in email 49 | addresses like `tom@example.com`. 50 | -} 51 | type alias Url = 52 | { protocol : Protocol 53 | , host : String 54 | , port_ : Maybe Int 55 | , path : String 56 | , query : Maybe String 57 | , fragment : Maybe String 58 | } 59 | 60 | 61 | {-| Is the URL served over a secure connection or not? 62 | -} 63 | type Protocol = Http | Https 64 | 65 | 66 | {-| Attempt to break a URL up into [`Url`](#Url). This is useful in 67 | single-page apps when you want to parse certain chunks of a URL to figure out 68 | what to show on screen. 69 | 70 | fromString "https://example.com:443" 71 | -- Just 72 | -- { protocol = Https 73 | -- , host = "example.com" 74 | -- , port_ = Just 443 75 | -- , path = "/" 76 | -- , query = Nothing 77 | -- , fragment = Nothing 78 | -- } 79 | 80 | fromString "https://example.com/hats?q=top%20hat" 81 | -- Just 82 | -- { protocol = Https 83 | -- , host = "example.com" 84 | -- , port_ = Nothing 85 | -- , path = "/hats" 86 | -- , query = Just "q=top%20hat" 87 | -- , fragment = Nothing 88 | -- } 89 | 90 | fromString "http://example.com/core/List/#map" 91 | -- Just 92 | -- { protocol = Http 93 | -- , host = "example.com" 94 | -- , port_ = Nothing 95 | -- , path = "/core/List/" 96 | -- , query = Nothing 97 | -- , fragment = Just "map" 98 | -- } 99 | 100 | The conversion to segments can fail in some cases as well: 101 | 102 | fromString "example.com:443" == Nothing -- no protocol 103 | fromString "http://tom@example.com" == Nothing -- userinfo disallowed 104 | fromString "http://#cats" == Nothing -- no host 105 | 106 | **Note:** This function does not use [`percentDecode`](#percentDecode) anything. 107 | It just splits things up. [`Url.Parser`](Url-Parser) actually _needs_ the raw 108 | `query` string to parse it properly. Otherwise it could get confused about `=` 109 | and `&` characters! 110 | -} 111 | fromString : String -> Maybe Url 112 | fromString str = 113 | if String.startsWith "http://" str then 114 | chompAfterProtocol Http (String.dropLeft 7 str) 115 | 116 | else if String.startsWith "https://" str then 117 | chompAfterProtocol Https (String.dropLeft 8 str) 118 | 119 | else 120 | Nothing 121 | 122 | 123 | chompAfterProtocol : Protocol -> String -> Maybe Url 124 | chompAfterProtocol protocol str = 125 | if String.isEmpty str then 126 | Nothing 127 | else 128 | case String.indexes "#" str of 129 | [] -> 130 | chompBeforeFragment protocol Nothing str 131 | 132 | i :: _ -> 133 | chompBeforeFragment protocol (Just (String.dropLeft (i + 1) str)) (String.left i str) 134 | 135 | 136 | chompBeforeFragment : Protocol -> Maybe String -> String -> Maybe Url 137 | chompBeforeFragment protocol frag str = 138 | if String.isEmpty str then 139 | Nothing 140 | else 141 | case String.indexes "?" str of 142 | [] -> 143 | chompBeforeQuery protocol Nothing frag str 144 | 145 | i :: _ -> 146 | chompBeforeQuery protocol (Just (String.dropLeft (i + 1) str)) frag (String.left i str) 147 | 148 | 149 | chompBeforeQuery : Protocol -> Maybe String -> Maybe String -> String -> Maybe Url 150 | chompBeforeQuery protocol params frag str = 151 | if String.isEmpty str then 152 | Nothing 153 | else 154 | case String.indexes "/" str of 155 | [] -> 156 | chompBeforePath protocol "/" params frag str 157 | 158 | i :: _ -> 159 | chompBeforePath protocol (String.dropLeft i str) params frag (String.left i str) 160 | 161 | 162 | chompBeforePath : Protocol -> String -> Maybe String -> Maybe String -> String -> Maybe Url 163 | chompBeforePath protocol path params frag str = 164 | if String.isEmpty str || String.contains "@" str then 165 | Nothing 166 | else 167 | case String.indexes ":" str of 168 | [] -> 169 | Just <| Url protocol str Nothing path params frag 170 | 171 | i :: [] -> 172 | case String.toInt (String.dropLeft (i + 1) str) of 173 | Nothing -> 174 | Nothing 175 | 176 | port_ -> 177 | Just <| Url protocol (String.left i str) port_ path params frag 178 | 179 | _ -> 180 | Nothing 181 | 182 | 183 | {-| Turn a [`Url`](#Url) into a `String`. 184 | -} 185 | toString : Url -> String 186 | toString url = 187 | let 188 | http = 189 | case url.protocol of 190 | Http -> 191 | "http://" 192 | 193 | Https -> 194 | "https://" 195 | in 196 | addPort url.port_ (http ++ url.host) ++ url.path 197 | |> addPrefixed "?" url.query 198 | |> addPrefixed "#" url.fragment 199 | 200 | 201 | addPort : Maybe Int -> String -> String 202 | addPort maybePort starter = 203 | case maybePort of 204 | Nothing -> 205 | starter 206 | 207 | Just port_ -> 208 | starter ++ ":" ++ String.fromInt port_ 209 | 210 | 211 | addPrefixed : String -> Maybe String -> String -> String 212 | addPrefixed prefix maybeSegment starter = 213 | case maybeSegment of 214 | Nothing -> 215 | starter 216 | 217 | Just segment -> 218 | starter ++ prefix ++ segment 219 | 220 | 221 | 222 | -- PERCENT ENCODING 223 | 224 | 225 | {-| **Use [Url.Builder](Url-Builder) instead!** Functions like `absolute`, 226 | `relative`, and `crossOrigin` already do this automatically! `percentEncode` 227 | is only available so that extremely custom cases are possible, if needed. 228 | 229 | Percent-encoding is how [the official URI spec][uri] “escapes” special 230 | characters. You can still represent a `?` even though it is reserved for 231 | queries. 232 | 233 | This function exists in case you want to do something extra custom. Here are 234 | some examples: 235 | 236 | -- standard ASCII encoding 237 | percentEncode "hat" == "hat" 238 | percentEncode "to be" == "to%20be" 239 | percentEncode "99%" == "99%25" 240 | 241 | -- non-standard, but widely accepted, UTF-8 encoding 242 | percentEncode "$" == "%24" 243 | percentEncode "¢" == "%C2%A2" 244 | percentEncode "€" == "%E2%82%AC" 245 | 246 | This is the same behavior as JavaScript's [`encodeURIComponent`][js] function, 247 | and the rules are described in more detail officially [here][s2] and with some 248 | notes about Unicode [here][wiki]. 249 | 250 | [js]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent 251 | [uri]: https://tools.ietf.org/html/rfc3986 252 | [s2]: https://tools.ietf.org/html/rfc3986#section-2.1 253 | [wiki]: https://en.wikipedia.org/wiki/Percent-encoding 254 | -} 255 | percentEncode : String -> String 256 | percentEncode = 257 | Elm.Kernel.Url.percentEncode 258 | 259 | 260 | {-| **Use [Url.Parser](Url-Parser) instead!** It will decode query 261 | parameters appropriately already! `percentDecode` is only available so that 262 | extremely custom cases are possible, if needed. 263 | 264 | Check out the `percentEncode` function to learn about percent-encoding. 265 | This function does the opposite! Here are the reverse examples: 266 | 267 | -- ASCII 268 | percentDecode "hat" == Just "hat" 269 | percentDecode "to%20be" == Just "to be" 270 | percentDecode "99%25" == Just "99%" 271 | 272 | -- UTF-8 273 | percentDecode "%24" == Just "$" 274 | percentDecode "%C2%A2" == Just "¢" 275 | percentDecode "%E2%82%AC" == Just "€" 276 | 277 | Why is it a `Maybe` though? Well, these strings come from strangers on the 278 | internet as a bunch of bits and may have encoding problems. For example: 279 | 280 | percentDecode "%" == Nothing -- not followed by two hex digits 281 | percentDecode "%XY" == Nothing -- not followed by two HEX digits 282 | percentDecode "%C2" == Nothing -- half of the "¢" encoding "%C2%A2" 283 | 284 | This is the same behavior as JavaScript's [`decodeURIComponent`][js] function. 285 | 286 | [js]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent 287 | -} 288 | percentDecode : String -> Maybe String 289 | percentDecode = 290 | Elm.Kernel.Url.percentDecode 291 | -------------------------------------------------------------------------------- /src/Url/Builder.elm: -------------------------------------------------------------------------------- 1 | module Url.Builder exposing 2 | ( absolute, relative, crossOrigin, custom, Root(..) 3 | , QueryParameter, string, int, toQuery 4 | ) 5 | 6 | 7 | {-| In [the URI spec](https://tools.ietf.org/html/rfc3986), Tim Berners-Lee 8 | says a URL looks like this: 9 | 10 | ``` 11 | https://example.com:8042/over/there?name=ferret#nose 12 | \___/ \______________/\_________/ \_________/ \__/ 13 | | | | | | 14 | scheme authority path query fragment 15 | ``` 16 | 17 | This module helps you create these! 18 | 19 | 20 | # Builders 21 | @docs absolute, relative, crossOrigin, custom, Root 22 | 23 | # Queries 24 | @docs QueryParameter, string, int, toQuery 25 | 26 | -} 27 | 28 | 29 | import Url 30 | 31 | 32 | 33 | -- BUILDERS 34 | 35 | 36 | {-| Create an absolute URL: 37 | 38 | absolute [] [] 39 | -- "/" 40 | 41 | absolute [ "packages", "elm", "core" ] [] 42 | -- "/packages/elm/core" 43 | 44 | absolute [ "blog", String.fromInt 42 ] [] 45 | -- "/blog/42" 46 | 47 | absolute [ "products" ] [ string "search" "hat", int "page" 2 ] 48 | -- "/products?search=hat&page=2" 49 | 50 | Notice that the URLs start with a slash! 51 | -} 52 | absolute : List String -> List QueryParameter -> String 53 | absolute pathSegments parameters = 54 | "/" ++ String.join "/" pathSegments ++ toQuery parameters 55 | 56 | 57 | {-| Create a relative URL: 58 | 59 | relative [] [] 60 | -- "" 61 | 62 | relative [ "elm", "core" ] [] 63 | -- "elm/core" 64 | 65 | relative [ "blog", String.fromInt 42 ] [] 66 | -- "blog/42" 67 | 68 | relative [ "products" ] [ string "search" "hat", int "page" 2 ] 69 | -- "products?search=hat&page=2" 70 | 71 | Notice that the URLs **do not** start with a slash! 72 | -} 73 | relative : List String -> List QueryParameter -> String 74 | relative pathSegments parameters = 75 | String.join "/" pathSegments ++ toQuery parameters 76 | 77 | 78 | {-| Create a cross-origin URL. 79 | 80 | crossOrigin "https://example.com" [ "products" ] [] 81 | -- "https://example.com/products" 82 | 83 | crossOrigin "https://example.com" [] [] 84 | -- "https://example.com/" 85 | 86 | crossOrigin 87 | "https://example.com:8042" 88 | [ "over", "there" ] 89 | [ string "name" "ferret" ] 90 | -- "https://example.com:8042/over/there?name=ferret" 91 | 92 | **Note:** Cross-origin requests are slightly restricted for security. 93 | For example, the [same-origin policy][sop] applies when sending HTTP requests, 94 | so the appropriate `Access-Control-Allow-Origin` header must be enabled on the 95 | *server* to get things working. Read more about the security rules [here][cors]. 96 | 97 | [sop]: https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy 98 | [cors]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS 99 | -} 100 | crossOrigin : String -> List String -> List QueryParameter -> String 101 | crossOrigin prePath pathSegments parameters = 102 | prePath ++ "/" ++ String.join "/" pathSegments ++ toQuery parameters 103 | 104 | 105 | 106 | -- CUSTOM BUILDER 107 | 108 | 109 | {-| Specify whether a [`custom`](#custom) URL is absolute, relative, or 110 | cross-origin. 111 | -} 112 | type Root = Absolute | Relative | CrossOrigin String 113 | 114 | 115 | {-| Create custom URLs that may have a hash on the end: 116 | 117 | custom Absolute 118 | [ "packages", "elm", "core", "latest", "String" ] 119 | [] 120 | (Just "length") 121 | -- "/packages/elm/core/latest/String#length" 122 | 123 | custom Relative [ "there" ] [ string "name" "ferret" ] Nothing 124 | -- "there?name=ferret" 125 | 126 | custom 127 | (CrossOrigin "https://example.com:8042") 128 | [ "over", "there" ] 129 | [ string "name" "ferret" ] 130 | (Just "nose") 131 | -- "https://example.com:8042/over/there?name=ferret#nose" 132 | -} 133 | custom : Root -> List String -> List QueryParameter -> Maybe String -> String 134 | custom root pathSegments parameters maybeFragment = 135 | let 136 | fragmentless = 137 | rootToPrePath root ++ String.join "/" pathSegments ++ toQuery parameters 138 | in 139 | case maybeFragment of 140 | Nothing -> 141 | fragmentless 142 | 143 | Just fragment -> 144 | fragmentless ++ "#" ++ fragment 145 | 146 | 147 | rootToPrePath : Root -> String 148 | rootToPrePath root = 149 | case root of 150 | Absolute -> 151 | "/" 152 | 153 | Relative -> 154 | "" 155 | 156 | CrossOrigin prePath -> 157 | prePath ++ "/" 158 | 159 | 160 | 161 | -- QUERY PARAMETERS 162 | 163 | 164 | {-| Represents query parameter. Builder functions like `absolute` percent-encode 165 | all the query parameters they get, so you do not need to worry about it! 166 | -} 167 | type QueryParameter = 168 | QueryParameter String String 169 | 170 | 171 | {-| Create a percent-encoded query parameter. 172 | 173 | absolute ["products"] [ string "search" "hat" ] 174 | -- "/products?search=hat" 175 | 176 | absolute ["products"] [ string "search" "coffee table" ] 177 | -- "/products?search=coffee%20table" 178 | -} 179 | string : String -> String -> QueryParameter 180 | string key value = 181 | QueryParameter (Url.percentEncode key) (Url.percentEncode value) 182 | 183 | 184 | {-| Create a percent-encoded query parameter. 185 | 186 | absolute ["products"] [ string "search" "hat", int "page" 2 ] 187 | -- "/products?search=hat&page=2" 188 | 189 | Writing `int key n` is the same as writing `string key (String.fromInt n)`. 190 | So this is just a convenience function, making your code a bit shorter! 191 | -} 192 | int : String -> Int -> QueryParameter 193 | int key value = 194 | QueryParameter (Url.percentEncode key) (String.fromInt value) 195 | 196 | 197 | {-| Convert a list of query parameters to a percent-encoded query. This 198 | function is used by `absolute`, `relative`, etc. 199 | 200 | toQuery [ string "search" "hat" ] 201 | -- "?search=hat" 202 | 203 | toQuery [ string "search" "coffee table" ] 204 | -- "?search=coffee%20table" 205 | 206 | toQuery [ string "search" "hat", int "page" 2 ] 207 | -- "?search=hat&page=2" 208 | 209 | toQuery [] 210 | -- "" 211 | -} 212 | toQuery : List QueryParameter -> String 213 | toQuery parameters = 214 | case parameters of 215 | [] -> 216 | "" 217 | 218 | _ -> 219 | "?" ++ String.join "&" (List.map toQueryPair parameters) 220 | 221 | 222 | toQueryPair : QueryParameter -> String 223 | toQueryPair (QueryParameter key value) = 224 | key ++ "=" ++ value 225 | -------------------------------------------------------------------------------- /src/Url/Parser.elm: -------------------------------------------------------------------------------- 1 | module Url.Parser exposing 2 | ( Parser, string, int, s 3 | , (), map, oneOf, top, custom 4 | , (), query 5 | , fragment 6 | , parse 7 | ) 8 | 9 | {-| In [the URI spec](https://tools.ietf.org/html/rfc3986), Tim Berners-Lee 10 | says a URL looks like this: 11 | 12 | ``` 13 | https://example.com:8042/over/there?name=ferret#nose 14 | \___/ \______________/\_________/ \_________/ \__/ 15 | | | | | | 16 | scheme authority path query fragment 17 | ``` 18 | 19 | This module is primarily for parsing the `path` part. 20 | 21 | 22 | # Primitives 23 | @docs Parser, string, int, s 24 | 25 | # Path 26 | @docs (), map, oneOf, top, custom 27 | 28 | # Query 29 | @docs (), query 30 | 31 | # Fragment 32 | @docs fragment 33 | 34 | # Run Parsers 35 | @docs parse 36 | 37 | -} 38 | 39 | import Dict exposing (Dict) 40 | import Url exposing (Url) 41 | import Url.Parser.Query as Query 42 | import Url.Parser.Internal as Q 43 | 44 | 45 | 46 | -- INFIX TABLE 47 | 48 | 49 | infix right 7 () = slash 50 | infix left 8 () = questionMark 51 | 52 | 53 | 54 | -- PARSERS 55 | 56 | 57 | {-| Turn URLs like `/blog/42/cat-herding-techniques` into nice Elm data. 58 | -} 59 | type Parser a b = 60 | Parser (State a -> List (State b)) 61 | 62 | 63 | type alias State value = 64 | { visited : List String 65 | , unvisited : List String 66 | , params : Dict String (List String) 67 | , frag : Maybe String 68 | , value : value 69 | } 70 | 71 | 72 | 73 | -- PARSE SEGMENTS 74 | 75 | 76 | {-| Parse a segment of the path as a `String`. 77 | 78 | -- /alice/ ==> Just "alice" 79 | -- /bob ==> Just "bob" 80 | -- /42/ ==> Just "42" 81 | -- / ==> Nothing 82 | -} 83 | string : Parser (String -> a) a 84 | string = 85 | custom "STRING" Just 86 | 87 | 88 | {-| Parse a segment of the path as an `Int`. 89 | 90 | -- /alice/ ==> Nothing 91 | -- /bob ==> Nothing 92 | -- /42/ ==> Just 42 93 | -- / ==> Nothing 94 | -} 95 | int : Parser (Int -> a) a 96 | int = 97 | custom "NUMBER" String.toInt 98 | 99 | 100 | {-| Parse a segment of the path if it matches a given string. It is almost 101 | always used with [``](#) or [`oneOf`](#oneOf). For example: 102 | 103 | blog : Parser (Int -> a) a 104 | blog = 105 | s "blog" int 106 | 107 | -- /blog/42 ==> Just 42 108 | -- /tree/42 ==> Nothing 109 | 110 | The path segment must be an exact match! 111 | -} 112 | s : String -> Parser a a 113 | s str = 114 | Parser <| \{ visited, unvisited, params, frag, value } -> 115 | case unvisited of 116 | [] -> 117 | [] 118 | 119 | next :: rest -> 120 | if next == str then 121 | [ State (next :: visited) rest params frag value ] 122 | 123 | else 124 | [] 125 | 126 | 127 | {-| Create a custom path segment parser. Here is how it is used to define the 128 | `int` parser: 129 | 130 | int : Parser (Int -> a) a 131 | int = 132 | custom "NUMBER" String.toInt 133 | 134 | You can use it to define something like “only CSS files” like this: 135 | 136 | css : Parser (String -> a) a 137 | css = 138 | custom "CSS_FILE" <| \segment -> 139 | if String.endsWith ".css" segment then 140 | Just segment 141 | else 142 | Nothing 143 | -} 144 | custom : String -> (String -> Maybe a) -> Parser (a -> b) b 145 | custom tipe stringToSomething = 146 | Parser <| \{ visited, unvisited, params, frag, value } -> 147 | case unvisited of 148 | [] -> 149 | [] 150 | 151 | next :: rest -> 152 | case stringToSomething next of 153 | Just nextValue -> 154 | [ State (next :: visited) rest params frag (value nextValue) ] 155 | 156 | Nothing -> 157 | [] 158 | 159 | 160 | 161 | -- COMBINING PARSERS 162 | 163 | 164 | {-| Parse a path with multiple segments. 165 | 166 | blog : Parser (Int -> a) a 167 | blog = 168 | s "blog" int 169 | 170 | -- /blog/35/ ==> Just 35 171 | -- /blog/42 ==> Just 42 172 | -- /blog/ ==> Nothing 173 | -- /42/ ==> Nothing 174 | 175 | search : Parser (String -> a) a 176 | search = 177 | s "search" string 178 | 179 | -- /search/wolf/ ==> Just "wolf" 180 | -- /search/frog ==> Just "frog" 181 | -- /search/ ==> Nothing 182 | -- /wolf/ ==> Nothing 183 | -} 184 | slash : Parser a b -> Parser b c -> Parser a c 185 | slash (Parser parseBefore) (Parser parseAfter) = 186 | Parser <| \state -> 187 | List.concatMap parseAfter (parseBefore state) 188 | 189 | 190 | {-| Transform a path parser. 191 | 192 | type alias Comment = { user : String, id : Int } 193 | 194 | userAndId : Parser (String -> Int -> a) a 195 | userAndId = 196 | s "user" string s "comment" int 197 | 198 | comment : Parser (Comment -> a) a 199 | comment = 200 | map Comment userAndId 201 | 202 | -- /user/bob/comment/42 ==> Just { user = "bob", id = 42 } 203 | -- /user/tom/comment/35 ==> Just { user = "tom", id = 35 } 204 | -- /user/sam/ ==> Nothing 205 | -} 206 | map : a -> Parser a b -> Parser (b -> c) c 207 | map subValue (Parser parseArg) = 208 | Parser <| \{ visited, unvisited, params, frag, value } -> 209 | List.map (mapState value) <| parseArg <| 210 | State visited unvisited params frag subValue 211 | 212 | 213 | mapState : (a -> b) -> State a -> State b 214 | mapState func { visited, unvisited, params, frag, value } = 215 | State visited unvisited params frag (func value) 216 | 217 | 218 | {-| Try a bunch of different path parsers. 219 | 220 | type Route 221 | = Topic String 222 | | Blog Int 223 | | User String 224 | | Comment String Int 225 | 226 | route : Parser (Route -> a) a 227 | route = 228 | oneOf 229 | [ map Topic (s "topic" string) 230 | , map Blog (s "blog" int) 231 | , map User (s "user" string) 232 | , map Comment (s "user" string s "comment" int) 233 | ] 234 | 235 | -- /topic/wolf ==> Just (Topic "wolf") 236 | -- /topic/ ==> Nothing 237 | 238 | -- /blog/42 ==> Just (Blog 42) 239 | -- /blog/wolf ==> Nothing 240 | 241 | -- /user/sam/ ==> Just (User "sam") 242 | -- /user/bob/comment/42 ==> Just (Comment "bob" 42) 243 | -- /user/tom/comment/35 ==> Just (Comment "tom" 35) 244 | -- /user/ ==> Nothing 245 | 246 | If there are multiple parsers that could succeed, the first one wins. 247 | -} 248 | oneOf : List (Parser a b) -> Parser a b 249 | oneOf parsers = 250 | Parser <| \state -> 251 | List.concatMap (\(Parser parser) -> parser state) parsers 252 | 253 | 254 | {-| A parser that does not consume any path segments. 255 | 256 | type Route = Overview | Post Int 257 | 258 | blog : Parser (BlogRoute -> a) a 259 | blog = 260 | s "blog" 261 | oneOf 262 | [ map Overview top 263 | , map Post (s "post" int) 264 | ] 265 | 266 | -- /blog/ ==> Just Overview 267 | -- /blog/post/42 ==> Just (Post 42) 268 | -} 269 | top : Parser a a 270 | top = 271 | Parser <| \state -> [state] 272 | 273 | 274 | 275 | -- QUERY 276 | 277 | 278 | {-| The [`Url.Parser.Query`](Url-Parser-Query) module defines its own 279 | [`Parser`](Url-Parser-Query#Parser) type. This function helps you use those 280 | with normal parsers. For example, maybe you want to add a search feature to 281 | your blog website: 282 | 283 | import Url.Parser.Query as Query 284 | 285 | type Route 286 | = Overview (Maybe String) 287 | | Post Int 288 | 289 | blog : Parser (Route -> a) a 290 | blog = 291 | oneOf 292 | [ map Overview (s "blog" Query.string "q") 293 | , map Post (s "blog" int) 294 | ] 295 | 296 | -- /blog/ ==> Just (Overview Nothing) 297 | -- /blog/?q=wolf ==> Just (Overview (Just "wolf")) 298 | -- /blog/wolf ==> Nothing 299 | -- /blog/42 ==> Just (Post 42) 300 | -- /blog/42?q=wolf ==> Just (Post 42) 301 | -- /blog/42/wolf ==> Nothing 302 | -} 303 | questionMark : Parser a (query -> b) -> Query.Parser query -> Parser a b 304 | questionMark parser queryParser = 305 | slash parser (query queryParser) 306 | 307 | 308 | {-| The [`Url.Parser.Query`](Url-Parser-Query) module defines its own 309 | [`Parser`](Url-Parser-Query#Parser) type. This function is a helper to convert 310 | those into normal parsers. 311 | 312 | import Url.Parser.Query as Query 313 | 314 | -- the following expressions are both the same! 315 | -- 316 | -- s "blog" Query.string "search" 317 | -- s "blog" query (Query.string "search") 318 | 319 | This may be handy if you need query parameters but are not parsing any path 320 | segments. 321 | -} 322 | query : Query.Parser query -> Parser (query -> a) a 323 | query (Q.Parser queryParser) = 324 | Parser <| \{ visited, unvisited, params, frag, value } -> 325 | [ State visited unvisited params frag (value (queryParser params)) 326 | ] 327 | 328 | 329 | 330 | -- FRAGMENT 331 | 332 | 333 | {-| Create a parser for the URL fragment, the stuff after the `#`. This can 334 | be handy for handling links to DOM elements within a page. Pages like this one! 335 | 336 | type alias Docs = 337 | (String, Maybe String) 338 | 339 | docs : Parser (Docs -> a) a 340 | docs = 341 | map Tuple.pair (string fragment identity) 342 | 343 | -- /List/map ==> Nothing 344 | -- /List/#map ==> Just ("List", Just "map") 345 | -- /List#map ==> Just ("List", Just "map") 346 | -- /List# ==> Just ("List", Just "") 347 | -- /List ==> Just ("List", Nothing) 348 | -- / ==> Nothing 349 | 350 | -} 351 | fragment : (Maybe String -> fragment) -> Parser (fragment -> a) a 352 | fragment toFrag = 353 | Parser <| \{ visited, unvisited, params, frag, value } -> 354 | [ State visited unvisited params frag (value (toFrag frag)) 355 | ] 356 | 357 | 358 | 359 | -- PARSE 360 | 361 | 362 | {-| Actually run a parser! You provide some [`Url`](Url#Url) that 363 | represent a valid URL. From there `parse` runs your parser on the path, query 364 | parameters, and fragment. 365 | 366 | import Url 367 | import Url.Parser exposing (Parser, parse, int, map, oneOf, s, top) 368 | 369 | type Route = Home | Blog Int | NotFound 370 | 371 | route : Parser (Route -> a) a 372 | route = 373 | oneOf 374 | [ map Home top 375 | , map Blog (s "blog" int) 376 | ] 377 | 378 | toRoute : String -> Route 379 | toRoute string = 380 | case Url.fromString string of 381 | Nothing -> 382 | NotFound 383 | 384 | Just url -> 385 | Maybe.withDefault NotFound (parse route url) 386 | 387 | -- toRoute "/blog/42" == NotFound 388 | -- toRoute "https://example.com/" == Home 389 | -- toRoute "https://example.com/blog" == NotFound 390 | -- toRoute "https://example.com/blog/42" == Blog 42 391 | -- toRoute "https://example.com/blog/42/" == Blog 42 392 | -- toRoute "https://example.com/blog/42#wolf" == Blog 42 393 | -- toRoute "https://example.com/blog/42?q=wolf" == Blog 42 394 | -- toRoute "https://example.com/settings" == NotFound 395 | 396 | Functions like `toRoute` are useful when creating single-page apps with 397 | [`Browser.application`][app]. I use them in `init` and `onNavigation` to handle 398 | the initial URL and any changes. 399 | 400 | [app]: /packages/elm/browser/latest/Browser#application 401 | -} 402 | parse : Parser (a -> a) a -> Url -> Maybe a 403 | parse (Parser parser) url = 404 | getFirstMatch <| parser <| 405 | State [] (preparePath url.path) (prepareQuery url.query) url.fragment identity 406 | 407 | 408 | getFirstMatch : List (State a) -> Maybe a 409 | getFirstMatch states = 410 | case states of 411 | [] -> 412 | Nothing 413 | 414 | state :: rest -> 415 | case state.unvisited of 416 | [] -> 417 | Just state.value 418 | 419 | [""] -> 420 | Just state.value 421 | 422 | _ -> 423 | getFirstMatch rest 424 | 425 | 426 | 427 | -- PREPARE PATH 428 | 429 | 430 | preparePath : String -> List String 431 | preparePath path = 432 | case String.split "/" path of 433 | "" :: segments -> 434 | removeFinalEmpty segments 435 | 436 | segments -> 437 | removeFinalEmpty segments 438 | 439 | 440 | removeFinalEmpty : List String -> List String 441 | removeFinalEmpty segments = 442 | case segments of 443 | [] -> 444 | [] 445 | 446 | "" :: [] -> 447 | [] 448 | 449 | segment :: rest -> 450 | segment :: removeFinalEmpty rest 451 | 452 | 453 | 454 | -- PREPARE QUERY 455 | 456 | 457 | prepareQuery : Maybe String -> Dict String (List String) 458 | prepareQuery maybeQuery = 459 | case maybeQuery of 460 | Nothing -> 461 | Dict.empty 462 | 463 | Just qry -> 464 | List.foldr addParam Dict.empty (String.split "&" qry) 465 | 466 | 467 | addParam : String -> Dict String (List String) -> Dict String (List String) 468 | addParam segment dict = 469 | case String.split "=" segment of 470 | [rawKey, rawValue] -> 471 | case Url.percentDecode rawKey of 472 | Nothing -> 473 | dict 474 | 475 | Just key -> 476 | case Url.percentDecode rawValue of 477 | Nothing -> 478 | dict 479 | 480 | Just value -> 481 | Dict.update key (addToParametersHelp value) dict 482 | 483 | _ -> 484 | dict 485 | 486 | 487 | addToParametersHelp : a -> Maybe (List a) -> Maybe (List a) 488 | addToParametersHelp value maybeList = 489 | case maybeList of 490 | Nothing -> 491 | Just [value] 492 | 493 | Just list -> 494 | Just (value :: list) 495 | -------------------------------------------------------------------------------- /src/Url/Parser/Internal.elm: -------------------------------------------------------------------------------- 1 | module Url.Parser.Internal exposing 2 | ( QueryParser(..) 3 | ) 4 | 5 | 6 | import Dict 7 | 8 | 9 | type QueryParser a = 10 | Parser (Dict.Dict String (List String) -> a) 11 | -------------------------------------------------------------------------------- /src/Url/Parser/Query.elm: -------------------------------------------------------------------------------- 1 | module Url.Parser.Query exposing 2 | ( Parser, string, int, enum, custom 3 | , map, map2, map3, map4, map5, map6, map7, map8 4 | ) 5 | 6 | {-| In [the URI spec](https://tools.ietf.org/html/rfc3986), Tim Berners-Lee 7 | says a URL looks like this: 8 | 9 | ``` 10 | https://example.com:8042/over/there?name=ferret#nose 11 | \___/ \______________/\_________/ \_________/ \__/ 12 | | | | | | 13 | scheme authority path query fragment 14 | ``` 15 | 16 | This module is for parsing the `query` part. 17 | 18 | In this library, a valid query looks like `?search=hats&page=2` where each 19 | query parameter has the format `key=value` and is separated from the next 20 | parameter by the `&` character. 21 | 22 | # Parse Query Parameters 23 | @docs Parser, string, int, enum, custom 24 | 25 | # Mapping 26 | @docs map, map2, map3, map4, map5, map6, map7, map8 27 | 28 | -} 29 | 30 | import Dict 31 | import Url.Parser.Internal as Q 32 | 33 | 34 | 35 | -- PARSERS 36 | 37 | 38 | {-| Parse a query like `?search=hat&page=2` into nice Elm data. 39 | -} 40 | type alias Parser a = 41 | Q.QueryParser a 42 | 43 | 44 | 45 | -- PRIMITIVES 46 | 47 | 48 | {-| Handle `String` parameters. 49 | 50 | search : Parser (Maybe String) 51 | search = 52 | string "search" 53 | 54 | -- ?search=cats == Just "cats" 55 | -- ?search=42 == Just "42" 56 | -- ?branch=left == Nothing 57 | -- ?search=cats&search=dogs == Nothing 58 | 59 | Check out [`custom`](#custom) if you need to handle multiple `search` 60 | parameters for some reason. 61 | -} 62 | string : String -> Parser (Maybe String) 63 | string key = 64 | custom key <| \stringList -> 65 | case stringList of 66 | [str] -> 67 | Just str 68 | 69 | _ -> 70 | Nothing 71 | 72 | 73 | {-| Handle `Int` parameters. Maybe you want to show paginated search results: 74 | 75 | page : Parser (Maybe Int) 76 | page = 77 | int "page" 78 | 79 | -- ?page=2 == Just 2 80 | -- ?page=17 == Just 17 81 | -- ?page=two == Nothing 82 | -- ?sort=date == Nothing 83 | -- ?page=2&page=3 == Nothing 84 | 85 | Check out [`custom`](#custom) if you need to handle multiple `page` parameters 86 | or something like that. 87 | -} 88 | int : String -> Parser (Maybe Int) 89 | int key = 90 | custom key <| \stringList -> 91 | case stringList of 92 | [str] -> 93 | String.toInt str 94 | 95 | _ -> 96 | Nothing 97 | 98 | 99 | {-| Handle enumerated parameters. Maybe you want a true-or-false parameter: 100 | 101 | import Dict 102 | 103 | debug : Parser (Maybe Bool) 104 | debug = 105 | enum "debug" (Dict.fromList [ ("true", True), ("false", False) ]) 106 | 107 | -- ?debug=true == Just True 108 | -- ?debug=false == Just False 109 | -- ?debug=1 == Nothing 110 | -- ?debug=0 == Nothing 111 | -- ?true=true == Nothing 112 | -- ?debug=true&debug=true == Nothing 113 | 114 | You could add `0` and `1` to the dictionary if you want to handle those as 115 | well. You can also use [`map`](#map) to say `map (Maybe.withDefault False) debug` 116 | to get a parser of type `Parser Bool` that swallows any errors and defaults to 117 | `False`. 118 | 119 | **Note:** Parameters like `?debug` with no `=` are not supported by this library. 120 | -} 121 | enum : String -> Dict.Dict String a -> Parser (Maybe a) 122 | enum key dict = 123 | custom key <| \stringList -> 124 | case stringList of 125 | [str] -> 126 | Dict.get str dict 127 | 128 | _ -> 129 | Nothing 130 | 131 | 132 | 133 | -- CUSTOM PARSERS 134 | 135 | 136 | {-| Create a custom query parser. The [`string`](#string), [`int`](#int), and 137 | [`enum`](#enum) parsers are defined using this function. It can help you handle 138 | anything though! 139 | 140 | Say you are unlucky enough to need to handle `?post=2&post=7` to show a couple 141 | posts on screen at once. You could say: 142 | 143 | posts : Parser (List Int) 144 | posts = 145 | custom "post" (List.filterMap String.toInt) 146 | 147 | -- ?post=2 == [2] 148 | -- ?post=2&post=7 == [2, 7] 149 | -- ?post=2&post=x == [2] 150 | -- ?hats=2 == [] 151 | -} 152 | custom : String -> (List String -> a) -> Parser a 153 | custom key func = 154 | Q.Parser <| \dict -> 155 | func (Maybe.withDefault [] (Dict.get key dict)) 156 | 157 | 158 | 159 | -- MAPPING 160 | 161 | 162 | {-| Transform a parser in some way. Maybe you want your `page` query parser to 163 | default to `1` if there is any problem? 164 | 165 | page : Parser Int 166 | page = 167 | map (Maybe.withDefault 1) (int "page") 168 | 169 | -} 170 | map : (a -> b) -> Parser a -> Parser b 171 | map func (Q.Parser a) = 172 | Q.Parser <| \dict -> func (a dict) 173 | 174 | 175 | {-| Combine two parsers. A query like `?search=hats&page=2` could be parsed 176 | with something like this: 177 | 178 | type alias Query = 179 | { search : Maybe String 180 | , page : Maybe Int 181 | } 182 | 183 | query : Parser Query 184 | query = 185 | map2 Query (string "search") (int "page") 186 | 187 | -} 188 | map2 : (a -> b -> result) -> Parser a -> Parser b -> Parser result 189 | map2 func (Q.Parser a) (Q.Parser b) = 190 | Q.Parser <| \dict -> 191 | func (a dict) (b dict) 192 | 193 | 194 | {-| Combine three parsers. A query like `?search=hats&page=2&sort=ascending` 195 | could be parsed with something like this: 196 | 197 | import Dict 198 | 199 | type alias Query = 200 | { search : Maybe String 201 | , page : Maybe Int 202 | , sort : Maybe Order 203 | } 204 | 205 | type Order = Ascending | Descending 206 | 207 | query : Parser Query 208 | query = 209 | map3 Query (string "search") (int "page") (enum "sort" order) 210 | 211 | order : Dict.Dict String Order 212 | order = 213 | Dict.fromList 214 | [ ( "ascending", Ascending ) 215 | , ( "descending", Descending ) 216 | ] 217 | -} 218 | map3 : (a -> b -> c -> result) -> Parser a -> Parser b -> Parser c -> Parser result 219 | map3 func (Q.Parser a) (Q.Parser b) (Q.Parser c) = 220 | Q.Parser <| \dict -> 221 | func (a dict) (b dict) (c dict) 222 | 223 | 224 | {-|-} 225 | map4 : (a -> b -> c -> d -> result) -> Parser a -> Parser b -> Parser c -> Parser d -> Parser result 226 | map4 func (Q.Parser a) (Q.Parser b) (Q.Parser c) (Q.Parser d) = 227 | Q.Parser <| \dict -> 228 | func (a dict) (b dict) (c dict) (d dict) 229 | 230 | 231 | 232 | {-|-} 233 | map5 : (a -> b -> c -> d -> e -> result) -> Parser a -> Parser b -> Parser c -> Parser d -> Parser e -> Parser result 234 | map5 func (Q.Parser a) (Q.Parser b) (Q.Parser c) (Q.Parser d) (Q.Parser e) = 235 | Q.Parser <| \dict -> 236 | func (a dict) (b dict) (c dict) (d dict) (e dict) 237 | 238 | 239 | {-|-} 240 | map6 : (a -> b -> c -> d -> e -> f -> result) -> Parser a -> Parser b -> Parser c -> Parser d -> Parser e -> Parser f -> Parser result 241 | map6 func (Q.Parser a) (Q.Parser b) (Q.Parser c) (Q.Parser d) (Q.Parser e) (Q.Parser f) = 242 | Q.Parser <| \dict -> 243 | func (a dict) (b dict) (c dict) (d dict) (e dict) (f dict) 244 | 245 | 246 | 247 | {-|-} 248 | map7 : (a -> b -> c -> d -> e -> f -> g -> result) -> Parser a -> Parser b -> Parser c -> Parser d -> Parser e -> Parser f -> Parser g -> Parser result 249 | map7 func (Q.Parser a) (Q.Parser b) (Q.Parser c) (Q.Parser d) (Q.Parser e) (Q.Parser f) (Q.Parser g) = 250 | Q.Parser <| \dict -> 251 | func (a dict) (b dict) (c dict) (d dict) (e dict) (f dict) (g dict) 252 | 253 | 254 | 255 | {-| If you need higher than eight, you can define a function like this: 256 | 257 | apply : Parser a -> Parser (a -> b) -> Parser b 258 | apply argParser funcParser = 259 | map2 (<|) funcParser argParser 260 | 261 | And then you can chain it to do as many of these as you would like: 262 | 263 | map func (string "search") 264 | |> apply (int "page") 265 | |> apply (int "per-page") 266 | 267 | -} 268 | map8 : (a -> b -> c -> d -> e -> f -> g -> h -> result) -> Parser a -> Parser b -> Parser c -> Parser d -> Parser e -> Parser f -> Parser g -> Parser h -> Parser result 269 | map8 func (Q.Parser a) (Q.Parser b) (Q.Parser c) (Q.Parser d) (Q.Parser e) (Q.Parser f) (Q.Parser g) (Q.Parser h) = 270 | Q.Parser <| \dict -> 271 | func (a dict) (b dict) (c dict) (d dict) (e dict) (f dict) (g dict) (h dict) 272 | --------------------------------------------------------------------------------