diff options
Diffstat (limited to '')
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | Makefile | 11 | ||||
-rw-r--r-- | NOTES.md | 306 | ||||
-rw-r--r-- | README.md | 17 | ||||
-rw-r--r-- | global.ha | 2 | ||||
-rw-r--r-- | main.ha | 22 | ||||
-rw-r--r-- | req.ha | 104 | ||||
-rw-r--r-- | static/style.css | 410 | ||||
-rw-r--r-- | templates/_footer.htmpl | 2 | ||||
-rw-r--r-- | url.ha | 20 |
10 files changed, 865 insertions, 32 deletions
@@ -1,2 +1,3 @@ /forge -/templates.ha +/.templates.ha +/.version.ha @@ -1,5 +1,10 @@ -forge: main.ha templates.ha - hare build -o $@ . +forge: .version.ha .templates.ha *.ha + hare build $(HAREFLAGS) -o $@ . -templates.ha: templates/*.htmpl +.templates.ha: templates/*.htmpl htmplgen -o $@ $^ + +.version.ha: + printf 'def VERSION="%s";\n' $(shell git describe --tags --always --dirty) > $@ + +.PHONY: version.ha @@ -7,3 +7,309 @@ You will need the following dependencies: [various patches](https://lists.sr.ht/~sircmpwn/hare-dev/patches?search=from%3Arunxiyu+prefix%3Ahare-http) - [hare-htmpl](https://forge.runxiyu.org/hare/:/repos/hare-htmpl/) ([backup](https://git.sr.ht/~runxiyu/hare-htmpl)) + + +Also, you'll need various horrible patches for `net::uri` before that gets fixed: + +``` +diff --git a/net/uri/+test.ha b/net/uri/+test.ha +index 345f41ee..63272d52 100644 +--- a/net/uri/+test.ha ++++ b/net/uri/+test.ha +@@ -10,7 +10,7 @@ use net::ip; + uri { + scheme = "file", + host = "", +- path = "/my/path/to/file", ++ raw_path = "/my/path/to/file", + ... + }, + )!; +@@ -19,7 +19,7 @@ use net::ip; + uri { + scheme = "http", + host = "harelang.org", +- path = "/", ++ raw_path = "/", + ... + }, + )!; +@@ -38,7 +38,7 @@ use net::ip; + scheme = "ldap", + host = [13, 37, 73, 31]: ip::addr4, + port = 1234, +- path = "/", ++ raw_path = "/", + ... + }, + )!; +@@ -47,7 +47,7 @@ use net::ip; + uri { + scheme = "http", + host = ip::parse("::1")!, +- path = "/test", ++ raw_path = "/test", + ... + }, + )!; +@@ -58,7 +58,7 @@ use net::ip; + uri { + scheme = "urn", + host = "", +- path = "example:animal:ferret:nose", ++ raw_path = "example:animal:ferret:nose", + ... + }, + )!; +@@ -67,7 +67,7 @@ use net::ip; + uri { + scheme = "mailto", + host = "", +- path = "~sircmpwn/hare-dev@lists.sr.ht", ++ raw_path = "~sircmpwn/hare-dev@lists.sr.ht", + ... + }, + )!; +@@ -76,7 +76,7 @@ use net::ip; + uri { + scheme = "http", + host = "", +- path = "/foo/bar", ++ raw_path = "/foo/bar", + ... + }, + )!; +@@ -85,7 +85,7 @@ use net::ip; + uri { + scheme = "http", + host = "", +- path = "/", ++ raw_path = "/", + ... + }, + )!; +@@ -94,7 +94,7 @@ use net::ip; + uri { + scheme = "https", + host = "sr.ht", +- path = "/projects", ++ raw_path = "/projects", + query = "search=%23risc-v&sort=longest-active", + fragment = "foo", + ... +@@ -105,7 +105,7 @@ use net::ip; + uri { + scheme = "https", + host = "en.wiktionary.org", +- path = "/wiki/おはよう", ++ raw_path = "/wiki/%E3%81%8A%E3%81%AF%E3%82%88%E3%81%86", + fragment = "Japanese", + ... + } +@@ -135,11 +135,11 @@ use net::ip; + + @test fn percent_encoding() void = { + test_uri( +- "https://git%2esr.ht/~sircmpw%6e/hare#Build%20status", ++ "https://git.sr.ht/~sircmpwn/hare#Build%20status", + uri { + scheme = "https", + host = "git.sr.ht", +- path = "/~sircmpwn/hare", ++ raw_path = "/~sircmpwn/hare", + fragment = "Build status", + ... + }, +@@ -152,7 +152,7 @@ use net::ip; + uri { + scheme = "ldap", + host = ip::parse("2001:db8::7")!, +- path = "/c=GB", ++ raw_path = "/c=GB", + query = "objectClass?one", + ... + }, +@@ -161,11 +161,11 @@ use net::ip; + + // https://bugs.chromium.org/p/chromium/issues/detail?id=841105 + test_uri( +- "https://web-safety.net/..;@www.google.com:%3443", ++ "https://web-safety.net/..;@www.google.com:443", + uri { + scheme = "https", + host = "web-safety.net", +- path = "/..;@www.google.com:443", ++ raw_path = "/..;@www.google.com:443", + ... + }, + "https://web-safety.net/..;@www.google.com:443", +@@ -180,6 +180,7 @@ fn test_uri(in: str, expected_uri: uri, expected_str: str) (void | invalid) = { + const u = parse(in)?; + defer finish(&u); + ++ + assert_str(u.scheme, expected_uri.scheme); + match (u.host) { + case let s: str => +@@ -189,7 +190,7 @@ fn test_uri(in: str, expected_uri: uri, expected_str: str) (void | invalid) = { + }; + assert(u.port == expected_uri.port); + assert_str(u.userinfo, expected_uri.userinfo); +- assert_str(u.path, expected_uri.path); ++ assert_str(u.raw_path, expected_uri.raw_path); + assert_str(u.query, expected_uri.query); + assert_str(u.fragment, expected_uri.fragment); + +diff --git a/net/uri/fmt.ha b/net/uri/fmt.ha +index 48a43f24..07cb3f7b 100644 +--- a/net/uri/fmt.ha ++++ b/net/uri/fmt.ha +@@ -20,9 +20,9 @@ use strings; + // query = *( pchar / "/" / "?" ) + // fragment = *( pchar / "/" / "?" ) + +-def unres_host: str = "-._~!$&'()*+,;="; +-def unres_query_frag: str = "-._~!$&'()*+,;=:@/?"; +-def unres_path: str = "-._~!$&'()*+,;=:@/"; ++export def unres_host: str = "-._~!$&'()*+,;="; ++export def unres_query_frag: str = "-._~!$&'()*+,;=:@/?"; ++export def unres_path: str = "-._~!$&'()*+,;=:@/"; + + // Writes a formatted [[uri]] to an [[io::handle]]. Returns the number of bytes + // written. +@@ -63,10 +63,10 @@ export fn fmt(out: io::handle, u: *const uri) (size | io::error) = { + if (u.port != 0) { + n += fmt::fprintf(out, ":{}", u.port)?; + }; +- if (has_host && len(u.path) > 0 && !strings::hasprefix(u.path, '/')) { ++ if (has_host && len(u.raw_path) > 0 && !strings::hasprefix(u.raw_path, '/')) { + n += fmt::fprint(out, "/")?; + }; +- n += percent_encode(out, u.path, unres_path)?; ++ n += memio::concat(out, u.raw_path)?; + if (len(u.query) > 0) { + // Always percent-encoded, see parse and encodequery/decodequery + n += fmt::fprintf(out, "?{}", u.query)?; +@@ -92,7 +92,7 @@ fn fmtaddr(out: io::handle, addr: ip::addr) (size | io::error) = { + return n; + }; + +-fn percent_encode(out: io::handle, src: str, allowed: str) (size | io::error) = { ++export fn percent_encode(out: io::handle, src: str, allowed: str) (size | io::error) = { + let iter = strings::iter(src); + let n = 0z; + for (let r => strings::next(&iter)) { +diff --git a/net/uri/parse.ha b/net/uri/parse.ha +index f2522c01..e108bd75 100644 +--- a/net/uri/parse.ha ++++ b/net/uri/parse.ha +@@ -22,10 +22,10 @@ export fn parse(in: str) (uri | invalid) = { + defer if (!success) free(scheme); + + // Determine hier-part variant +- let path = ""; ++ let raw_path = ""; + let authority: ((str | ip::addr6), u16, str) = ("", 0u16, ""); + defer if (!success) { +- free(path); ++ free(raw_path); + free_host(authority.0); + free(authority.2); + }; +@@ -50,7 +50,7 @@ export fn parse(in: str) (uri | invalid) = { + case '/' => + // path-absolute + strings::prev(&in); +- path = parse_path(&in, ++ raw_path = parse_path(&in, + path_mode::ABSOLUTE)?; + case => + return invalid; +@@ -61,17 +61,17 @@ export fn parse(in: str) (uri | invalid) = { + // path-absolute + strings::prev(&in); // return current token + strings::prev(&in); // return leading slash +- path = parse_path(&in, path_mode::ABSOLUTE)?; ++ raw_path = parse_path(&in, path_mode::ABSOLUTE)?; + }; + case => + // path-absolute (just '/') + strings::prev(&in); // return leading slash +- path = parse_path(&in, path_mode::ABSOLUTE)?; ++ raw_path = parse_path(&in, path_mode::ABSOLUTE)?; + }; + case => + // path-rootless + strings::prev(&in); +- path = parse_path(&in, path_mode::ROOTLESS)?; ++ raw_path = parse_path(&in, path_mode::ROOTLESS)?; + }; + case => void; // path-empty + }; +@@ -118,7 +118,7 @@ export fn parse(in: str) (uri | invalid) = { + port = authority.1, + userinfo = authority.2, + +- path = path, ++ raw_path = raw_path, + query = query, + fragment = fragment, + }; +@@ -274,7 +274,7 @@ fn parse_path(in: *strings::iterator, mode: path_mode) (str | invalid) = { + }; + }; + +- return percent_decode(strings::slice(©, in)); ++ return strings::dup(strings::slice(©, in))!; + }; + + fn parse_query(in: *strings::iterator) (str | invalid) = { +@@ -323,13 +323,14 @@ fn parse_port(in: *strings::iterator) (u16 | invalid) = { + }; + }; + +-fn percent_decode(s: str) (str | invalid) = { ++// must be freed by caller ++export fn percent_decode(s: str) (str | invalid) = { + let buf = memio::dynamic(); + percent_decode_static(&buf, s)?; + return memio::string(&buf)!; + }; + +-fn percent_decode_static(out: io::handle, s: str) (void | invalid) = { ++export fn percent_decode_static(out: io::handle, s: str) (void | invalid) = { + let iter = strings::iter(s); + let tmp = memio::dynamic(); + defer io::close(&tmp)!; +diff --git a/net/uri/uri.ha b/net/uri/uri.ha +index 623ffafb..3b7b7c4c 100644 +--- a/net/uri/uri.ha ++++ b/net/uri/uri.ha +@@ -12,7 +12,7 @@ export type uri = struct { + port: u16, + userinfo: str, + +- path: str, ++ raw_path: str, + query: str, + fragment: str, + }; +@@ -31,7 +31,7 @@ export fn dup(u: *uri) uri = { + port = u.port, + userinfo = strings::dup(u.userinfo)!, + +- path = strings::dup(u.path)!, ++ raw_path = strings::dup(u.raw_path)!, + query = strings::dup(u.query)!, + fragment = strings::dup(u.fragment)!, + }; +@@ -46,7 +46,7 @@ export fn finish(u: *uri) void = { + case => void; + }; + free(u.userinfo); +- free(u.path); ++ free(u.raw_path); + free(u.query); + free(u.fragment); + }; +``` @@ -1,15 +1,2 @@ -# Lindenii Forge - -**Work in progress.** - -This is the new implementation in the [Hare](https://harelang.org) programming -language. We will set this as the primary branch once it reaches feature parity -with the Go implementation. - -## Architecture - -* Most components are one single daemon written in Hare. -* Because libssh is difficult to use and there aren't many other SSH server - libraries for C or Hare, we will temporarily use - [the gliberlabs SSH library for Go](https://github.com/gliderlabs/ssh) - in a separate process, and communicate via UNIX domain sockets. +**Please check out the `master` branch. Everything that used to be in the +`hare` branch is now in `master`. The `hare` branch is deprectated.** @@ -5,7 +5,7 @@ let global: struct { ssh_fp: str, } = struct { title: str = "Test Forge", - version: str = "v0.0.0", + version: str = VERSION, ssh_pubkey: str = "pubkey", ssh_fp: str = "fp", }; @@ -2,26 +2,28 @@ // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> // Adapted from template by Willow Barraco <contact@willowbarraco.fr> +use fs; use getopt; -use htmpl; use log; use net; use net::dial; use net::http; use net::ip; use net::tcp; +use net::uri; use os; use memio; use io; use fmt; use bufio; -use strings; const usage: [_]getopt::help = [ "Lindenii Forge Server", ('c', "config", "path to configuration file") ]; +let static_fs: nullable *fs::fs = null; + export fn main() void = { const cmd = getopt::parse(os::args, usage...); defer getopt::finish(&cmd); @@ -32,10 +34,12 @@ export fn main() void = { for (let opt .. cmd.opts) { switch (opt.0) { case 'c' => yield; // TODO: actually handle the config - case => abort(); // unreachable + case => abort("unreachable"); }; }; + static_fs = os::diropen("static")!; + const server = match (http::listen(ip_addr, port, net::tcp::reuseport, net::tcp::reuseaddr)) { case let this: *http::server => yield this; @@ -47,19 +51,15 @@ export fn main() void = { const serv_req = match (http::serve(server)) { case let this: *http::server_request => yield this; - case net::error => abort("failure while serving"); + case => + log::println("failure while serving"); + continue; }; defer http::serve_finish(serv_req); match (handlereq(serv_req.socket, &serv_req.request)) { case void => yield; - case io::error => log::println("error while handling request"); + case => log::println("error while handling request"); }; }; }; - -export fn handlereq(conn: io::handle, request: *http::request) (void | io::error | nomem) = { - htmpl::write(conn, "HTTP/1.1 200 OK\r\n")?; - htmpl::write(conn, "Content-Type: text/html\r\n\r\n")?; - tp_index(conn)?; -}; @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> + +use fmt; +use fs; +use htmpl; +use io; +use mime; +use net::http; +use net::uri; +use strconv; +use strings; + +fn handlereq(conn: io::handle, request: *http::request) (void | io::error | nomem | fs::error) = { + let segments = match(segments_from_path(request.target.raw_path)) { + case let s: []str => + yield s; + case uri::invalid => + start_response(conn, 400, "text/plain")?; + fmt::fprintln(conn, "Invalid URI")?; + return void; + case nomem => + return nomem; + case => + abort("unreachable"); + }; + defer strings::freeall(segments); + + let trailing_slash: bool = false; + + if (segments[len(segments) - 1] == "") { + trailing_slash = true; + free(segments[len(segments) - 1]); + segments = segments[.. len(segments) - 1]; + }; + + if (len(segments) == 0) { + start_response(conn, 200, "text/html")?; + return tp_index(conn); + }; + + if (segments[0] == ":") { + if (len(segments) == 1) { + start_response(conn, 404, "text/plain")?; + fmt::fprintln(conn, "Error: Blank system endpoint")?; + return; + }; + + switch (segments[1]) { + case "static" => + if (len(segments) == 2) { + start_response(conn, 404, "text/plain")?; + fmt::fprintln(conn, "Error: Blank static endpoint")?; + return; + }; + + let fs_segments = segments[2 ..]; + for (let fs_segment .. fs_segments) { + if (strings::contains(fs_segment, "/")) { + start_response(conn, 400, "text/plain")?; + fmt::fprintln(conn, "Error: Slash found in filesystem path")?; + return; + }; + }; + let fs_segment_path = strings::join("/", fs_segments...)?; + defer free(fs_segment_path); + + let file = match (fs::open(static_fs as *fs::fs, fs_segment_path)) { + case let f: io::handle => yield f; + case fs::error => + start_response(conn, 500, "text/plain")?; + fmt::fprintln(conn, "Filesystem error")?; + return; + }; + defer io::close(file)!; + + let ext = strings::rcut(fs_segments[len(fs_segments) - 1], ".").1; + + let mimetype = match (mime::lookup_ext(ext)) { + case let m: *mime::mimetype => yield m.mime; + case null => yield "application/octet-stream"; + }; + + start_response(conn, 200, mimetype)?; + io::copy(conn, file)?; + + case => + start_response(conn, 404, "text/plain")?; + fmt::fprintln(conn, "Error: Unknown system endpoint")?; + }; + }; +}; + +fn start_response(conn: io::handle, status: uint, content_type: str) (void | io::error | nomem) = { // TODO: add len and other headers + fmt::fprint(conn, "HTTP/1.1 ")?; + fmt::fprint(conn, strconv::utos(status))?; + fmt::fprint(conn, " ")?; + fmt::fprint(conn, http::status_reason(status))?; + fmt::fprint(conn, "\r\n")?; + fmt::fprint(conn, "Content-Type: ")?; + fmt::fprint(conn, content_type)?; + fmt::fprint(conn, "\r\n")?; + fmt::fprint(conn, "\r\n")?; +}; diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..e5398ce --- /dev/null +++ b/static/style.css @@ -0,0 +1,410 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * SPDX-FileContributor: Runxi Yu <https://runxiyu.org> + * SPDX-FileContributor: luk3yx <https://luk3yx.github.io> + */ + +/* Base styles and variables */ +html { + font-family: sans-serif; + background-color: var(--background-color); + color: var(--text-color); + --radius-1: 0.32rem; + --background-color: hsl(0, 0%, 100%); + --text-color: hsl(0, 0%, 0%); + --link-color: hsl(320, 50%, 36%); + --light-text-color: hsl(0, 0%, 45%); + --darker-border-color: hsl(0, 0%, 72%); + --lighter-border-color: hsl(0, 0%, 85%); + --text-decoration-color: hsl(0, 0%, 72%); + --darker-box-background-color: hsl(0, 0%, 92%); + --lighter-box-background-color: hsl(0, 0%, 95%); + --primary-color: hsl(320, 50%, 36%); + --primary-color-contrast: hsl(320, 0%, 100%); + --danger-color: #ff0000; + --danger-color-contrast: #ffffff; +} + +/* Dark mode overrides */ +@media (prefers-color-scheme: dark) { + html { + --background-color: hsl(0, 0%, 0%); + --text-color: hsl(0, 0%, 100%); + --link-color: hsl(320, 50%, 76%); + --light-text-color: hsl(0, 0%, 78%); + --darker-border-color: hsl(0, 0%, 35%); + --lighter-border-color: hsl(0, 0%, 25%); + --text-decoration-color: hsl(0, 0%, 30%); + --darker-box-background-color: hsl(0, 0%, 20%); + --lighter-box-background-color: hsl(0, 0%, 15%); + } +} + +/* Global layout */ +body { + margin: 0; +} +html, code, pre { + font-size: 0.96rem; /* TODO: Not always correct */ +} + +/* Toggle table controls */ +.toggle-table-off, .toggle-table-on { + opacity: 0; + position: absolute; +} +.toggle-table-off:focus-visible + table > thead > tr > th > label, +.toggle-table-on:focus-visible + table > thead > tr > th > label { + outline: 1.5px var(--primary-color) solid; +} +.toggle-table-off + table > thead > tr > th, .toggle-table-on + table > thead > tr > th { + padding: 0; +} +.toggle-table-off + table > thead > tr > th > label, .toggle-table-on + table > thead > tr > th > label { + width: 100%; + display: inline-block; + padding: 3px 0; + cursor: pointer; +} +.toggle-table-off:checked + table > tbody { + display: none; +} +.toggle-table-on + table > tbody { + display: none; +} +.toggle-table-on:checked + table > tbody { + display: table-row-group; +} + +table.rounded, table.rounded-footed { + overflow: hidden; + border-spacing: 0; + border-collapse: separate; + border-radius: var(--radius-1); + border: var(--lighter-border-color) solid 1px; +} + +table.rounded th, table.rounded td, +table.rounded-footed th, table.rounded-footed td { + border: none; +} + +table.rounded th:not(:last-child), +table.rounded td:not(:last-child), +table.rounded-footed th:not(:last-child), +table.rounded-footed td:not(:last-child) { + border-right: var(--lighter-border-color) solid 1px; +} + +table.rounded>thead>tr>th, +table.rounded>thead>tr>td, +table.rounded>tbody>tr:not(:last-child)>th, +table.rounded>tbody>tr:not(:last-child)>td { + border-bottom: var(--lighter-border-color) solid 1px; +} + +table.rounded-footed>thead>tr>th, +table.rounded-footed>thead>tr>td, +table.rounded-footed>tbody>tr>th, +table.rounded-footed>tbody>tr>td, +table.rounded-footed>tfoot>tr:not(:last-child)>th, +table.rounded-footed>tfoot>tr:not(:last-child)>td { + border-bottom: var(--lighter-border-color) solid 1px; +} + + +/* Footer styles */ +footer { + margin-top: 1rem; + margin-left: auto; + margin-right: auto; + display: block; + padding: 0 5px; + width: fit-content; + text-align: center; + color: var(--light-text-color); +} +footer a:link, footer a:visited { + color: inherit; +} + +/* Padding containers */ +.padding-wrapper { + margin: 1rem auto; + max-width: 60rem; + padding: 0 5px; +} +.padding { + padding: 0 5px; +} + +/* Link styles */ +a:link, a:visited { + text-decoration-color: var(--text-decoration-color); + color: var(--link-color); +} + +/* Readme inline code styling */ +#readme code:not(pre > code) { + background-color: var(--lighter-box-background-color); + border-radius: 2px; + padding: 2px; +} + +/* Readme word breaks to avoid overfull hboxes */ +#readme { + word-break: break-word; +} + +/* Table styles */ +table { + border: var(--lighter-border-color) solid 1px; + border-spacing: 0px; + border-collapse: collapse; +} +table.wide { + width: 100%; +} +td, th { + padding: 3px 5px; + border: var(--lighter-border-color) solid 1px; +} +.pad { + padding: 3px 5px; +} +th, thead, tfoot { + background-color: var(--lighter-box-background-color); +} +th[scope=row] { + text-align: left; +} +th { + font-weight: normal; +} +tr.title-row > th, th.title-row, .title-row { + background-color: var(--lighter-box-background-color); + font-weight: bold; +} +td > pre { + margin: 0; +} +#readme > *:last-child { + margin-bottom: 0; +} +#readme > *:first-child { + margin-top: 0; +} + +/* Table misc and scrolling */ +.commit-id { + font-family: monospace; + word-break: break-word; +} +.scroll { + overflow-x: auto; +} + +/* Diff/chunk styles */ +.chunk-unchanged { + color: grey; +} +.chunk-addition { + color: green; +} +@media (prefers-color-scheme: dark) { + .chunk-addition { + color: lime; + } +} +.chunk-deletion { + color: red; +} +.chunk-unknown { + color: yellow; +} +pre.chunk { + margin-top: 0; + margin-bottom: 0; +} +.centering { + text-align: center; +} + +/* Toggle content sections */ +.toggle-off-wrapper, .toggle-on-wrapper { + border: var(--lighter-border-color) solid 1px; +} +.toggle-off-toggle, .toggle-on-toggle { + opacity: 0; + position: absolute; +} +.toggle-off-header, .toggle-on-header { + font-weight: bold; + cursor: pointer; + display: block; + width: 100%; + background-color: var(--lighter-box-background-color); +} +.toggle-off-header > div, .toggle-on-header > div { + padding: 3px 5px; + display: block; +} +.toggle-on-content { + display: none; +} +.toggle-on-toggle:focus-visible + .toggle-on-header, .toggle-off-toggle:focus-visible + .toggle-off-header { + outline: 1.5px var(--primary-color) solid; +} +.toggle-on-toggle:checked + .toggle-on-header + .toggle-on-content { + display: block; +} +.toggle-off-content { + display: block; +} +.toggle-off-toggle:checked + .toggle-off-header + .toggle-off-content { + display: none; +} + +*:focus-visible { + outline: 1.5px var(--primary-color) solid; +} + +/* File display styles */ +.file-patch + .file-patch { + margin-top: 0.5rem; +} +.file-content { + padding: 3px 5px; +} +.file-header { + font-family: monospace; + display: flex; + flex-direction: row; + align-items: center; +} +.file-header::after { + content: "\25b6"; + font-family: sans-serif; + margin-left: auto; + line-height: 100%; + margin-right: 0.25em; +} +.file-toggle:checked + .file-header::after { + content: "\25bc"; +} + +/* Form elements */ +textarea { + box-sizing: border-box; + background-color: var(--lighter-box-background-color); + resize: vertical; +} +textarea, +input[type=text], +input[type=password] { + font-family: sans-serif; + font-size: smaller; + background-color: var(--lighter-box-background-color); + color: var(--text-color); + border: none; + padding: 0.3rem; + width: 100%; + box-sizing: border-box; +} +td.tdinput, th.tdinput { + padding: 0; + position: relative; +} +td.tdinput textarea, +td.tdinput input[type=text], +td.tdinput input[type=password], +th.tdinput textarea, +th.tdinput input[type=text], +th.tdinput input[type=password] { + background-color: transparent; +} +td.tdinput select { + position: absolute; + background-color: var(--background-color); + border: none; + /* + width: 100%; + height: 100%; + */ + box-sizing: border-box; + top: 0; + left: 0; + right: 0; + bottom: 0; +} +select:active { + outline: 1.5px var(--primary-color) solid; +} + + +/* Button styles */ +.btn-primary, a.btn-primary { + background: var(--primary-color); + color: var(--primary-color-contrast); + border: var(--lighter-border-color) 1px solid; + font-weight: bold; +} +.btn-danger, a.btn-danger { + background: var(--danger-color); + color: var(--danger-color-contrast); + border: var(--lighter-border-color) 1px solid; + font-weight: bold; +} +.btn-white, a.btn-white { + background: var(--primary-color-contrast); + color: var(--primary-color); + border: var(--lighter-border-color) 1px solid; +} +.btn-normal, a.btn-normal, +input[type=file]::file-selector-button { + background: var(--lighter-box-background-color); + border: var(--lighter-border-color) 1px solid !important; + color: var(--text-color); +} +.btn, .btn-white, .btn-danger, .btn-normal, .btn-primary, +input[type=submit], +input[type=file]::file-selector-button { + display: inline-block; + width: auto; + min-width: fit-content; + border-radius: var(--radius-1); + padding: .1rem .75rem; + font-size: 0.9rem; + transition: background .1s linear; + cursor: pointer; +} +a.btn, a.btn-white, a.btn-danger, a.btn-normal, a.btn-primary { + text-decoration: none; +} + +/* Header layout */ +header#main-header { + background-color: var(--lighter-box-background-color); + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; +} +header#main-header > div#main-header-forge-title { + flex-grow: 1; +} +header#main-header > div#main-header-user { + display: flex; + align-items: center; +} + +/* Uncategorized */ +table + table { + margin-top: 1rem; +} + +td > ul { + padding-left: 1.5rem; + margin-top: 0; + margin-bottom: 0; +} diff --git a/templates/_footer.htmpl b/templates/_footer.htmpl index 71d5318..a0d1987 100644 --- a/templates/_footer.htmpl +++ b/templates/_footer.htmpl @@ -5,5 +5,5 @@ {{ " " }} (<a href="/:/source/">source</a>, {{ " " }} -<a href="https://forge.lindenii.runxiyu.org/lindenii/forge/:/repos/server/">upstream</a>) +<a href="https://forge.lindenii.runxiyu.org/lindenii/forge/:/repos/server/?branch=hare">upstream</a>) {{ end }} @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> + +use strings; +use net::uri; + +// The result, if not erroring out, must be freed with strings::freeall. +fn segments_from_path(s: str) ([]str | nomem | uri::invalid) = { + let sp: []str = strings::split(s, "/")?; + for (let i = 1z; i < len(sp); i += 1) { + match (uri::percent_decode(sp[i])) { + case let s: str => + sp[i - 1] = s; + case uri::invalid => + strings::freeall(sp[.. i - 1]); + return uri::invalid; + }; + }; + return sp[.. len(sp) - 1]; +}; |