commit 0918440e346e322a76c8041e8939e8e7e70de0d9 from: Benjamin Stürz date: Mon Apr 01 16:20:44 2024 UTC Import commit - /dev/null commit + 0918440e346e322a76c8041e8939e8e7e70de0d9 blob - /dev/null blob + a79dad7e410e973d9cab54a49d4391bc0d1a66ae (mode 644) --- /dev/null +++ .gitignore @@ -0,0 +1,2 @@ +/target +/www.cgi blob - /dev/null blob + f5610c5363aaf340dd7b51649d04782260bf5d3b (mode 644) --- /dev/null +++ Cargo.lock @@ -0,0 +1,299 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" + +[[package]] +name = "bumpalo" +version = "3.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" + +[[package]] +name = "cc" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11a6ae1e52eb25aab8f3fb9fca13be982a373b8f1157ca14b897a825ba4a2d35" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + +[[package]] +name = "www-cgi" +version = "0.1.0" +dependencies = [ + "chrono", + "itertools", +] blob - /dev/null blob + edf52e3f33ca9a1b86120d49ec312c3d8221feb4 (mode 644) --- /dev/null +++ Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "www-cgi" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chrono = "0.4.37" +itertools = "0.12.1" blob - /dev/null blob + 66a34507a8648c546ed689da668190936ab6ce2a (mode 644) --- /dev/null +++ Makefile @@ -0,0 +1,24 @@ + +SRC != find src -name '*.rs' + +RUSTFLAGS = -C relocation-model=static \ + -L native=/usr/lib \ + -l static=c \ + -l static=c++abi \ + -l static=pthread \ + -C link-arg=-static \ + -C panic=abort + +all: www.cgi + +clean: + cargo clean + rm -f www.cgi + +install: www.cgi + scp www.cgi server:/var/www/bin/ + openrsync --rsync-path=/usr/bin/openrsync -tr --delete --exclude fix cats server:/var/www/htdocs/ + +www.cgi: ${SRC} src/style.css + cargo rustc --release -- ${RUSTFLAGS} + mv -f target/release/www-cgi www.cgi blob - /dev/null blob + 7bedcdc0bf2a3c12294b2082d086c7a6697774e3 (mode 644) Binary files /dev/null and cats/1.jpg differ blob - /dev/null blob + a79e1b8932a75749300d618aa171a0b6c7625a7d (mode 644) Binary files /dev/null and cats/10.jpg differ blob - /dev/null blob + b43f918169ce12d67f1cdc0d44a1b3689f7fdedb (mode 644) Binary files /dev/null and cats/11.jpg differ blob - /dev/null blob + 4998b5f31b54224588e6ee9d5974579c40a40d36 (mode 644) Binary files /dev/null and cats/12.jpg differ blob - /dev/null blob + cf7ba488e8766077d7ef10110ca61ee025958c55 (mode 644) Binary files /dev/null and cats/13.jpg differ blob - /dev/null blob + e4953de7b4091c16e7894961daf2bce4b0f555e0 (mode 644) Binary files /dev/null and cats/14.jpg differ blob - /dev/null blob + 9574078edfb79338cc2caba1ed8da6ddf40b6924 (mode 644) Binary files /dev/null and cats/15.jpg differ blob - /dev/null blob + 323579dd34f4be587b146451ea83b6dc6795e17c (mode 644) Binary files /dev/null and cats/16.jpg differ blob - /dev/null blob + 44d2bbaa1d866c126595a702ffd1e9c14fc60266 (mode 644) Binary files /dev/null and cats/17.jpg differ blob - /dev/null blob + 2bea3e4213191917739baa4a2d32cfb91aa77693 (mode 644) Binary files /dev/null and cats/18.jpg differ blob - /dev/null blob + dae59c6e7706d225d073c4cf7b5a0cf440a79c90 (mode 644) Binary files /dev/null and cats/19.jpg differ blob - /dev/null blob + 8375a0240277de48131bef446d7959b128d1fbf1 (mode 644) Binary files /dev/null and cats/2.jpg differ blob - /dev/null blob + b96e4ba3518b475dfb8a79259ea7ae9b6cbb46c1 (mode 644) Binary files /dev/null and cats/20.jpg differ blob - /dev/null blob + 376527b16c4236a21db689c093f99ccd69735e8f (mode 644) Binary files /dev/null and cats/21.jpg differ blob - /dev/null blob + bb269d0a6b70ff58bbca3725fb11af3b75acc093 (mode 644) Binary files /dev/null and cats/22.jpg differ blob - /dev/null blob + 50930ac894fcc6eaaa24e0d53cc38a7765df4128 (mode 644) Binary files /dev/null and cats/23.jpg differ blob - /dev/null blob + 44720e2cea2e70ca2b5ed8fd8edafc38b29eb8d8 (mode 644) Binary files /dev/null and cats/24.jpg differ blob - /dev/null blob + 5a76b8cffec005fe05721a6e8085e223814c4c41 (mode 644) Binary files /dev/null and cats/25.jpg differ blob - /dev/null blob + 9410cad4d5034dc88b56eddff546cf8d443de7e0 (mode 644) Binary files /dev/null and cats/26.jpg differ blob - /dev/null blob + bdd3117974a1753c25328db71767e62da730af6e (mode 644) Binary files /dev/null and cats/27.jpg differ blob - /dev/null blob + f870d288d92e6d96ecab53902d0d40654a31ad1a (mode 644) Binary files /dev/null and cats/28.jpg differ blob - /dev/null blob + 220d0e4135a019f04d308cdaba8d63429f9643cb (mode 644) Binary files /dev/null and cats/3.jpg differ blob - /dev/null blob + eb672b6b24d6368995be0cbf5f17411867799ba7 (mode 644) Binary files /dev/null and cats/4.jpg differ blob - /dev/null blob + c4882f6eef556b98b614c1b80b93ea797c80989d (mode 644) Binary files /dev/null and cats/5.jpg differ blob - /dev/null blob + 82c9d90af5cf146e0ace8fc3a1d84f4baa0c9d20 (mode 644) Binary files /dev/null and cats/6.jpg differ blob - /dev/null blob + f604acadf017b7a32322742608336ad5b13201d9 (mode 644) Binary files /dev/null and cats/7.jpg differ blob - /dev/null blob + d61e8544fe298c2f82aab8de4c4bbb81b1d13ada (mode 644) Binary files /dev/null and cats/8.jpg differ blob - /dev/null blob + 08bfbfab54d01d524ee06f8c03fb43c5720752ab (mode 644) Binary files /dev/null and cats/9.jpg differ blob - /dev/null blob + 9553ae49e957dc45fe0ab1dfe070911fd07c60e1 (mode 755) --- /dev/null +++ cats/fix @@ -0,0 +1,27 @@ +#!/bin/sh + +check() { + command -v "$1" > /dev/null || { echo "Please install '$1'" >&2; exit 1; } +} + +check 'convert' +check 'jpegoptim' +check 'exiftran' + +for png in *.png; do + [ "$png" = "*.png" ] && break + convert "$png" "$(basename "$png" .png).jpg" + rm -f "$png" +done + + +i=$(find . -name '*.jpg' -maxdepth 1 | sed -En 's#^(\./)?([0-9]+)\.jpg$#\2#p' | sort -n | tail -n1) +i=$((i + 1)) + +for f in *.jpg; do + echo "$f" | grep -qE '^[0-9]+\.jpg$' && continue + jpegoptim -sS1024 -T 25 "$f" + exiftran -ai "$f" + mv "$f" "$i.jpg" + i=$((i + 1)) +done blob - /dev/null blob + 631940df13cd97176e7206e4544b1c19dae1eae5 (mode 644) --- /dev/null +++ src/html.rs @@ -0,0 +1,184 @@ +use std::fmt::{self, Display, Formatter}; + +#[derive(Clone)] +pub struct Tag { + pub name: &'static str, + pub args: Vec<(&'static str, String)>, + pub children: Vec, + pub self_closing: bool, +} + +#[derive(Clone)] +pub enum Element { + Tag(Tag), + String(String), +} + +#[derive(Clone, Copy)] +pub struct Indent(u32); + +impl Element { + pub fn render(&self, f: &mut Formatter<'_>, ind: Indent) -> fmt::Result { + match self { + Self::Tag(Tag { name, args, children, self_closing }) => { + write!(f, "{ind}<{name}")?; + for (name, value) in args { + write!(f, " {name}={value:?}")?; + } + write!(f, ">")?; + match &children[..] { + [] if *self_closing => writeln!(f), + [] => writeln!(f, ""), + [Element::String(s)] => writeln!(f, "{s}"), + _ => { + writeln!(f)?; + + for child in children { + child.render(f, ind.next())?; + } + + writeln!(f, "{ind}")?; + Ok(()) + } + } + }, + Self::String(s) => writeln!(f, "{ind}{s}"), + } + } +} + +impl From<&str> for Element { + fn from(value: &str) -> Self { + Self::String(value.into()) + } +} + +impl From for Element { + fn from(value: String) -> Self { + Self::String(value) + } +} + +impl Display for Element { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.render(f, Indent(0)) + } +} + +impl Display for Indent { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + for _ in 0..self.0 { + write!(f, "\t")?; + } + Ok(()) + } +} + +impl Indent { + fn next(self) -> Self { + Self(self.0 + 1) + } +} + +#[macro_export] +macro_rules! html { + ($name:ident $([ $($an:tt = $av:expr),* $(,)? ])? { $($tk:tt)* }) => { + { + #[allow(unused_mut)] + let mut tag = crate::html::Tag { + name: stringify!($name), + args: vec! [ + $( + $( + (html!(@ $an), $av.to_string()) + ),* + )? + ], + children: Vec::new(), + self_closing: false, + }; + + html! { + ! tag $($tk)* + } + + crate::html::Element::Tag(tag) + } + }; + (! $parent:ident $name:ident $([$($args:tt)*])? { $($tk:tt)* } $($rest:tt)*) => { + $parent.children.push(html! { $name $([$($args)*])? { $($tk)* } }); + + html! { + ! $parent $($rest)* + } + }; + (! $parent:ident $name:ident $([$($an:ident = $av:expr),*])? ; $($rest:tt)*) => { + $parent.children.push( + crate::html::Element::Tag(crate::html::Tag { + name: stringify!($name), + args: vec! [ + $( + $( + (html! { @$an}, $av.to_string()) + ),* + )? + ], + children: Vec::new(), + self_closing: true, + }) + ); + + html! { + ! $parent $($rest)* + } + }; + (! $parent:ident $name:ident = $val:expr ; $($rest:tt)*) => { + $parent.children.push(crate::html::Element::Tag(crate::html::Tag { + name: stringify!($name), + args: Vec::new(), + children: vec![ + crate::html::Element::String($val.into()) + ], + self_closing: false, + })); + + html! { + ! $parent $($rest)* + } + }; + (! $parent:ident $s:literal ; $($rest:tt)*) => { + $parent.children.push(crate::html::Element::String(format!($s))); + + html! { + ! $parent $($rest)* + } + }; + (! $parent:ident { $e:expr } $($rest:tt)*) => { + $parent.children.push($e.into()); + + html! { + ! $parent $($rest)* + } + }; + (! $parent:ident ? { $e:expr } $($rest:tt)*) => { + if let Some(e) = $e { + $parent.children.push(e.into()); + } + + html! { + ! $parent $($rest)* + } + }; + (! $parent:ident [ $e:expr ] $($rest:tt)*) => { + for e in $e { + $parent.children.push(e.into()); + } + + html! { + ! $parent $($rest)* + } + }; + (! $parent:ident) => {}; + (@ $i:ident) => { stringify!($i) }; + (@ $s:literal) => { $s }; +} blob - /dev/null blob + f160afdc57b27872393d3c3419d1bd1ce8f4c135 (mode 644) --- /dev/null +++ src/main.rs @@ -0,0 +1,88 @@ +use crate::{html::Element, site::route}; + +mod site; +mod html; + +type Result> = std::result::Result; + +pub struct Page { + title: String, + html: Element, +} + +pub struct Request { + pub path: String, + pub query: String, +} + +fn parse() -> Result { + let (path, query) = if std::env::args().count() >= 2 { + let path = std::env::args().nth(1).unwrap(); + let query = std::env::args().nth(2).unwrap_or_else(String::new); + (path, query) + } else { + let path = std::env::var("DOCUMENT_URI")?; + let query = std::env::var("QUERY_STRING")?; + (path, query) + }; + + let path = path + .strip_prefix("/test") + .expect("failed to strip prefix") + .to_string(); + + Ok(Request { + path, + query, + }) +} + +fn main() -> Result<()> { + let req = parse()?; + let (page, ok) = match route(&req) { + Ok(page) => (page, true), + Err(e) => { + let page = Page { + title: "Internal Server Error".into(), + html: html! { + main { + h1 = "Internal Server Error"; + p { + "Sorry, an error occured."; + "Error: {e}"; + } + } + } + }; + (page, false) + }, + }; + + let top = html! { + html { + head { + title = format!("Test - {}", page.title); + style { + [ + include_str!("style.css") + .lines() + ] + } + } + body [style="background-color: #222; color: #ccc; font-size: 18px"] { + {site::menu()} + {page.html} + } + } + }; + + if !ok { + println!("Status: 500 Internal Server Error"); + } + println!("Content-Type: text/html"); + println!(); + println!(""); + println!("{top}"); + + Ok(()) +} blob - /dev/null blob + 2ecb2164438d24a4fe5c5ca38fe1496a88a3339e (mode 644) --- /dev/null +++ src/site/cats.rs @@ -0,0 +1,70 @@ +use crate::{site::Page, html, Result, Request}; + + +pub fn index(req: &Request) -> Result { + let mut max = 0u32; + let q = &req.query; + + for ent in std::fs::read_dir("/htdocs/cats")? { + let ent = ent?; + let name = ent.file_name(); + let name = name.to_string_lossy(); + let Some(name) = name.strip_suffix(".jpg") else { continue }; + let Ok(id) = name.parse() else { continue }; + if id > max { + max = id; + } + } + + if max == 0 { + return Ok(Page { + title: "Error".into(), + html: html! { + main { + h1 = "No Cat Pictures"; + p = "Sorry, I don't have any cat pictures at the moment."; + } + } + }); + } + + let id = q.parse().unwrap_or(1u32); + + let maybe_link = |cond, label: &str, target: String| { + if cond { + html! { + a [href=target] { + {label} + } + } + } else { + label.into() + } + }; + + let html = html! { + main [style="width: 1280px; margin-left: auto; margin-right: auto;"] { + h1 = "Cats"; + div [style="border: 1px solid #fff; margin-left: auto; margin-right: auto; display: inline-block; font-size: 20px; height=80vh"] { + a [href=format!("/cats/{id}.jpg")] { + img [src=format!("/cats/{id}.jpg"), alt="cat picture", style="width: auto; max-width: 100%; max-height: 80vh"]; + } + br; + div [align="center"] { + {maybe_link(id > 1, "First", ".".into())} + " | "; + {maybe_link(id > 1, "Prev", format!(".?{}", id - 1))} + " | "; + {maybe_link(id < max, "Next", format!(".?{}", id + 1))} + " | "; + {maybe_link(id < max, "Last", format!(".?{}", max))} + } + } + } + }; + + Ok(Page { + title: "Cats".into(), + html, + }) +} blob - /dev/null blob + 10fcd13692ebced9ea3e5396f2b6b4ad3eea3d98 (mode 644) --- /dev/null +++ src/site/index.rs @@ -0,0 +1,14 @@ +use crate::{Result, site::Page, html, Request}; + +pub fn index(_req: &Request) -> Result { + let html = html! { + main { + h1 = "Index"; + p = "This is a simple CGI test website."; + } + }; + Ok(Page { + title: "Index".into(), + html, + }) +} blob - /dev/null blob + e0bab8843979f0282e188af477668371bf19f95f (mode 644) --- /dev/null +++ src/site/mod.rs @@ -0,0 +1,99 @@ +use itertools::Itertools; +use crate::{Result, Page, Request, html, html::Element}; + +pub type Handler = fn(&Request) -> Result; + +struct Route { + prefix: &'static str, + handler: Handler, + label: Option<(&'static str, u32)>, +} + +mod index; +mod cats; +mod time; + +macro_rules! routes { + [$($prefix:literal => $handler:path $([$($label:tt)+])?),* $(,)?] => { + &[ + $( + routes!(! $prefix => $handler $([$($label)+])?) + ),* + ] + }; + (! $prefix:literal => $handler:path [$label:literal : $weight:expr]) => { + Route { + prefix: $prefix, + handler: $handler, + label: Some(($label, $weight)), + } + }; + (! $prefix:literal => $handler:path) => { + Route { + prefix: $prefix, + handler: $handler, + label: None, + } + }; +} + +fn not_found(req: &Request) -> Result { + let path = &req.path; + Ok(Page { + title: "Not Found".into(), + html: html! { + main { + h1 = "404 - Not Found"; + p { "Invalid path: {path:?}"; } + } + }, + }) +} + +const ROUTES: &[Route] = routes![ + "/time/" => time::index ["Time" : 2], + "/cats/" => cats::index ["Cats" : 1], + "/" => index::index ["Index" : 0], + "/*" => not_found, +]; + +fn matches(path: &str, pattern: &str) -> bool { + if pattern.ends_with("/*") { + path.ends_with(&pattern[..pattern.len() - 2]) + } else { + path == pattern + } +} + +fn find_route<'a>(req: &'a Request) -> &'a Route { + ROUTES + .iter() + .find(|r| matches(&req.path, r.prefix)) + .expect("failed to find route") +} + +pub fn route(req: &Request) -> Result { + (find_route(req).handler)(req) +} + +#[allow(unstable_name_collisions)] +pub fn menu() -> Element { + html! { + nav { + [ + ROUTES + .iter() + .filter_map(|r| r.label.as_ref().map(|l| (l, r.prefix))) + .map(|((l, w), p)| (l, w, p)) + .sorted_by(|(_, w1, _), (_, w2, _)| w1.cmp(w2)) + .map(|(l, _, p)| { + let p = p.strip_suffix('*').unwrap_or(p); + html! { + a [href=format!("/test{p}")] { {*l} } + } + }) + .intersperse(" | ".into()) + ] + } + } +} blob - /dev/null blob + 3f0d74e31198ba166fb4cbb438549557642ba6ce (mode 644) --- /dev/null +++ src/site/time.rs @@ -0,0 +1,24 @@ +use chrono::Local; +use crate::{Request, Result, Page, html}; + +pub fn index(_req: &Request) -> Result { + let time = Local::now().to_rfc2822(); + let script = r#" +async function init() { + console.log("Waiting..."); + await new Promise(res => setTimeout(res, 3000)); + location.reload(); +} +window.onload = init; +"#; + Ok(Page { + title: "Current Time".into(), + html: html! { + main { + script { "{script}"; } + h1 = "The Current System Time"; + p { "{time}"; } + } + }, + }) +} blob - /dev/null blob + d94c14ec7f2b62b5fe3b73ee78cf4d02798472af (mode 644) --- /dev/null +++ src/style.css @@ -0,0 +1,3 @@ +a { + color: dodgerblue; +}