///|
/// Represent the URL change request from the user, trigger by `@html.a` tag.
/// The `Internal` means the target URL is in the same domain.
/// The `External` means the target URL is in another site.
pub(all) enum UrlRequest {
Internal(Url)
External(String)
} derive(Debug, Eq, Compare)
///|
pub extend UrlRequest with Debug::{to_repr}
///|
pub extend UrlRequest with Eq::{equal, not_equal}
///|
pub extend UrlRequest with Compare::{compare, op_ge, op_gt, op_le, op_lt}
///|
pub impl Show for UrlRequest with fn output(self, buf) {
Show::output(@debug.Debug::to_repr(self), buf)
}
///|
pub extend UrlRequest with Show::{output, to_string}
///|
/// Url
///
/// ```text
/// https://example.com:8042/over/there?name=ferret#nose
/// \___/ \______________/\_________/ \_________/ \__/
/// | | | | |
/// scheme authority path query fragment
/// ```
///
/// This diagram is from https://package.elm-lang.org/packages/elm/url/latest/Url
pub(all) struct Url {
protocol : Protocol
host : String
port : Int?
path : String
query : String?
fragment : String?
} derive(Debug, Eq, Compare)
///|
pub extend Url with Debug::{to_repr}
///|
pub extend Url with Eq::{equal, not_equal}
///|
pub extend Url with Compare::{compare, op_ge, op_gt, op_le, op_lt}
///|
pub impl Show for Url with fn output(self, buf) {
Show::output(@debug.Debug::to_repr(self), buf)
}
///|
pub extend Url with Show::{output}
///|
pub(all) enum Protocol {
Http
Https
Other(String)
} derive(Debug, Eq, Compare)
///|
pub extend Protocol with Debug::{to_repr}
///|
pub extend Protocol with Eq::{equal, not_equal}
///|
pub extend Protocol with Compare::{compare, op_ge, op_gt, op_le, op_lt}
///|
pub impl Show for Protocol with fn output(self, buf) {
Show::output(@debug.Debug::to_repr(self), buf)
}
///|
pub extend Protocol with Show::{output, to_string}
///|
pub fn Url::to_string(self : Url) -> String {
let protocol = match self.protocol {
Http => "http"
Https => "https"
_ => panic() // TODO: fix this
}
let port = match self.port {
Some(p) => ":\{p}"
None => ""
}
let query = match self.query {
Some(q) => "?\{q}"
None => ""
}
let fragment = match self.fragment {
Some(f) => "#\{f}"
None => ""
}
"\{protocol}://\{self.host}\{port}/\{self.path}\{query}\{fragment}"
}
///|
pub fn parse(url : String) -> Url raise Error {
// ⚠️ **先切 `#`,再在它之前那半段切 `?`**(2026-10-07 修)。
//
// 老实现反着来(先 `?` 再在"问号之前"找 `#`),于是 `#/s/baz?s=sec-2` 被拆成
// "query 在外、fragment 在内" —— **与真实地址不符**,`to_string()` 还回去是另一条 URL。
// 真浏览器实测(`examples/apps/route-spike/`):
// 地址栏 `http://h/#/s/baz?s=sec-2&v=raw` ⇒ 老实现读出 `?s=sec-2&v=raw` + `#/s/baz`。
// RFC 3986 里 **fragment 是最后一段、可以含 `?`**,所以 `#` 必须先切。
let (protocol, remain) = match url.split("://").collect() {
["http", remain] => (Http, remain)
["https", remain] => (Https, remain)
[x, remain] => (Other(x.to_owned()), remain)
[remain] => (Other(""), remain)
_ => fail("Invalid protocol")
}
// ① 先切 fragment(第一个 `#` 之后**全部**算 fragment,允许里面再有 `#` 与 `?`)
// `[head, .. rest]` 已经覆盖了"没有 `#`"(那时 rest 为空)⇒ 不需要单独的 `[head]` 分支
// (实测加了它只会得到一条 `unreachable_code` 警告)。
let (before_hash, fragment) = match remain.split("#").collect() {
[head, .. rest] =>
if rest.is_empty() {
(head, None)
} else {
(head, Some(rest.iter().map(StringView::to_owned).join("#")))
}
_ => fail("Invalid fragment")
}
// ② 再在 `#` 之前那半段里切 query(没有 `?` ⇒ 没有 query)
let (mid, query) = match before_hash.split("?").collect() {
[mid, q] => (mid, if q.is_empty() { None } else { Some(q.to_owned()) })
[mid] => (mid, None)
_ => fail("Invalid query")
}
let (mid, path) = match mid.split("/").collect() {
[mid] => (mid, "")
[mid, .. paths] => (mid, paths.iter().map(StringView::to_owned).join("/"))
_ => fail("Invalid host")
}
let (host, port) = match mid.split(":").collect() {
[host, port] => {
let port = try @string.parse_int(port.to_owned()) catch {
_ => Option::None
} noraise {
number => Some(number)
}
(host.to_owned(), port)
}
[host] => (host.to_owned(), None)
_ => fail("Invalid host")
}
{ protocol, host, port, path, query, fragment, }
}