std.url
URLs (RFC 3986), written in heff: a URL read from its text into its
parts (parse), written back (render), a reference resolved against
the URL it is in (resolve, RFC 3986 §5.2), and the escapes of its
parts and of a query’s keys and values. Where the RFC leaves a choice,
Go’s net/url is the model. The parts are kept as they are written,
escapes and all: unescape reads one.
import std.url as URL
-- | >>> next "https://example.com/docs/a/index.html" "../b/"-- | Just ("https://example.com/docs/b/")next : String -> String -> Maybe Stringnext page link = case URL.parse page of Nothing -> Nothing Just base -> maybe Nothing (\ref -> Just (URL.render (URL.resolve base ref))) (URL.parse link)URL
type URL = { scheme : String , user : Maybe String , host : String , port : Maybe Int , path : String , query : Maybe String , fragment : Maybe String }A URL, or a reference relative to one, in its parts as they are
written (escaped): the scheme ("" in a relative reference), the
user information before an @, the host (an IPv6 address without
its brackets), the port, the path, and the query and the fragment,
without their ? and #.
escapeQuery
escapeQuery : String -> StringA key or a value of a query escaped, as Go’s QueryEscape: a space
+, a letter, a digit, -, ., _ and ~ as they are, every
other byte %XX.
>>> escapeQuery "a b&c=d/é""a+b%26c%3Dd%2F%C3%A9">>> escapeQuery "-_.~+""-_.~%2B"escapePath
escapePath : String -> StringA segment of a path escaped, as Go’s PathEscape escapes it: a
letter, a digit, -, ., _, ~ and $&+:=@ as they are, every
other byte %XX — a space %20, a / %2F, a ; %3B.
>>> escapePath "a b/c?d#e""a%20b%2Fc%3Fd%23e">>> escapePath "x=1;y+z@h:p!$&'()*,~""x=1%3By+z@h:p%21$&%27%28%29%2A%2C~"unescape
unescape : String -> Maybe StringEach escape %XX (in either case) the byte it is, a + kept;
nothing if a % is not followed by two hex digits.
>>> unescape "a%20b+c%2fd%C3%A9"Just ("a b+c/dé")>>> unescape "100%"Nothing>>> unescape "%zz"NothingunescapeQuery
unescapeQuery : String -> Maybe StringA key or a value of a query read: each escape %XX the byte it is,
each + a space; nothing if a % is not followed by two hex digits.
>>> unescapeQuery "a+b%2Bc"Just ("a b+c")>>> unescapeQuery "%4"Nothingparse
parse : String -> Maybe URLThe URL, or the relative reference, s writes, if it writes one:
a scheme (a letter, then letters, digits, +, - and .) and :,
then perhaps // and an authority (user information and @, a
host — an IPv6 address in brackets — and : and a port, its
digits), a path, perhaps ? and a query, and perhaps # and a
fragment. Nothing if the scheme or the port is not one, if a %
is not followed by two hex digits, or if a byte is a control
character. An empty port is none.
>>> parse "https://ann:pw@example.com:8080/a/b%2Fc?x=1&y=2#top"Just ({ scheme = "https", user = Just ("ann:pw"), host = "example.com", port = Just (8080), path = "/a/b%2Fc", query = Just ("x=1&y=2"), fragment = Just ("top") })>>> parse "http://[::1]:80/"Just ({ scheme = "http", user = Nothing, host = "::1", port = Just (80), path = "/", query = Nothing, fragment = Nothing })>>> parse "mailto:ann@example.com"Just ({ scheme = "mailto", user = Nothing, host = "", port = Nothing, path = "ann@example.com", query = Nothing, fragment = Nothing })>>> parse "../a?q#"Just ({ scheme = "", user = Nothing, host = "", port = Nothing, path = "../a", query = Just ("q"), fragment = Just ("") })>>> parse "//example.com"Just ({ scheme = "", user = Nothing, host = "example.com", port = Nothing, path = "", query = Nothing, fragment = Nothing })>>> (List.map (\s -> maybe True (\_ -> False) (parse s)) ["http://a:8x/", "1http://a", ":x", "a b:c", "http://a/%zz", "http://a/%2", "http://a/\n", "http://[::1", "http://[::1]x", "http://[a]/", "http://a]/"] : List Bool)[True, True, True, True, True, True, True, True, True, True, True]render
render : URL -> StringThe text of u: parse’s inverse on what it reads, but that a URL
with a scheme and a path from the root has an authority written, if
an empty one, as Go writes it (file:/a is file:///a), and that
an empty port is not written. An IPv6 host (one with a :) is
written in brackets; a path is written from the root after an
authority, and after ./ where its first segment would be read as a
scheme.
>>> List.map (\s -> maybe "" render (parse s)) ["https://ann:pw@example.com:8080/a/b%2Fc?x=1&y=2#top", "http://[::1]:80/", "mailto:ann@example.com", "../a?q#", "//example.com", "file:///etc/hosts", "http:////x", "?", "#f", ""]["https://ann:pw@example.com:8080/a/b%2Fc?x=1&y=2#top", "http://[::1]:80/", "mailto:ann@example.com", "../a?q#", "//example.com", "file:///etc/hosts", "http:////x", "?", "#f", ""]>>> List.map (\s -> maybe "" render (parse s)) ["file:/etc/hosts", "http://a:/"]["file:///etc/hosts", "http://a/"]>>> render { scheme = "http", user = Nothing, host = "fe80::1", port = Just 8080, path = "x", query = Nothing, fragment = Nothing }"http://[fe80::1]:8080/x">>> render { scheme = "", user = Nothing, host = "", port = Nothing, path = "a:b/c", query = Nothing, fragment = Nothing }"./a:b/c"resolve
resolve : URL -> URL -> URLThe URL the reference ref names, read where base is, as RFC 3986
§5.2 resolves one (strictly: a scheme in ref is its own, even
base’s): ref’s parts from the first it has on, base’s before
them, a relative path merged with base’s, and the . and ..
segments of the path taken away.
>>> ((let resolved : String -> String = \r -> maybe "" (\b -> maybe "" (\x -> render (resolve b x)) (parse r)) (parse "http://a/b/c/d;p?q") in List.map resolved ["g:h", "g", "./g", "g/", "/g", "//g", "?y", "g?y"]) : List String)["g:h", "http://a/b/c/g", "http://a/b/c/g", "http://a/b/c/g/", "http://a/g", "http://g", "http://a/b/c/d;p?y", "http://a/b/c/g?y"]>>> ((let resolved : String -> String = \r -> maybe "" (\b -> maybe "" (\x -> render (resolve b x)) (parse r)) (parse "http://a/b/c/d;p?q") in List.map resolved ["#s", "g#s", "g?y#s", ";x", "g;x", "g;x?y#s", ""]) : List String)["http://a/b/c/d;p?q#s", "http://a/b/c/g#s", "http://a/b/c/g?y#s", "http://a/b/c/;x", "http://a/b/c/g;x", "http://a/b/c/g;x?y#s", "http://a/b/c/d;p?q"]>>> ((let resolved : String -> String = \r -> maybe "" (\b -> maybe "" (\x -> render (resolve b x)) (parse r)) (parse "http://a/b/c/d;p?q") in List.map resolved [".", "./", "..", "../", "../g", "../..", "../../", "../../g"]) : List String)["http://a/b/c/", "http://a/b/c/", "http://a/b/", "http://a/b/", "http://a/b/g", "http://a/", "http://a/", "http://a/g"]>>> ((let resolved : String -> String = \r -> maybe "" (\b -> maybe "" (\x -> render (resolve b x)) (parse r)) (parse "http://a/b/c/d;p?q") in List.map resolved ["../../../g", "../../../../g", "/./g", "/../g", "g.", ".g", "g..", "..g"]) : List String)["http://a/g", "http://a/g", "http://a/g", "http://a/g", "http://a/b/c/g.", "http://a/b/c/.g", "http://a/b/c/g..", "http://a/b/c/..g"]>>> ((let resolved : String -> String = \r -> maybe "" (\b -> maybe "" (\x -> render (resolve b x)) (parse r)) (parse "http://a/b/c/d;p?q") in List.map resolved ["./../g", "./g/.", "g/./h", "g/../h", "g;x=1/./y", "g;x=1/../y", "g?y/./x", "g#s/../x", "http:g"]) : List String)["http://a/b/g", "http://a/b/c/g/", "http://a/b/c/g/h", "http://a/b/c/h", "http://a/b/c/g;x=1/y", "http://a/b/c/y", "http://a/b/c/g?y/./x", "http://a/b/c/g#s/../x", "http:g"]>>> maybe "" (\b -> maybe "" (\x -> render (resolve b x)) (parse "a/./b")) (parse "http://h")"http://h/a/b"parseQuery
parseQuery : String -> Maybe (List (String, String))A query’s keys and values, in order: its pieces between &s (an
empty one left out), each a key, and = and a value ("" if it has
none), both read by unescapeQuery; nothing if one does not read.
Only & separates two (a ; is no separator, but text).
>>> parseQuery "a=1&b=x+y%21&a=3&flag&&=v&k=a=b"Just ([MkPair ("a") ("1"), MkPair ("b") ("x y!"), MkPair ("a") ("3"), MkPair ("flag") (""), MkPair ("") ("v"), MkPair ("k") ("a=b")])>>> parseQuery ""Just ([])>>> parseQuery "a=%zz"NothingrenderQuery
renderQuery : List (String, String) -> StringA query of the keys and values, in the order given, each key=value
escaped by escapeQuery, joined by &.
>>> renderQuery [MkPair "q" "heff lang", MkPair "page" "2", MkPair "x&y" ""]"q=heff+lang&page=2&x%26y=">>> parseQuery (renderQuery [MkPair "a b" "c=d&e", MkPair "" "+"])Just ([MkPair ("a b") ("c=d&e"), MkPair ("") ("+")])