commit - /dev/null
commit + 0918440e346e322a76c8041e8939e8e7e70de0d9
blob - /dev/null
blob + a79dad7e410e973d9cab54a49d4391bc0d1a66ae (mode 644)
--- /dev/null
+++ .gitignore
+/target
+/www.cgi
blob - /dev/null
blob + f5610c5363aaf340dd7b51649d04782260bf5d3b (mode 644)
--- /dev/null
+++ Cargo.lock
+# 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
+[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
+
+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
+#!/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
+use std::fmt::{self, Display, Formatter};
+
+#[derive(Clone)]
+pub struct Tag {
+ pub name: &'static str,
+ pub args: Vec<(&'static str, String)>,
+ pub children: Vec<Element>,
+ 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, "</{name}>"),
+ [Element::String(s)] => writeln!(f, "{s}</{name}>"),
+ _ => {
+ writeln!(f)?;
+
+ for child in children {
+ child.render(f, ind.next())?;
+ }
+
+ writeln!(f, "{ind}</{name}>")?;
+ Ok(())
+ }
+ }
+ },
+ Self::String(s) => writeln!(f, "{ind}{s}"),
+ }
+ }
+}
+
+impl From<&str> for Element {
+ fn from(value: &str) -> Self {
+ Self::String(value.into())
+ }
+}
+
+impl From<String> 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
+use crate::{html::Element, site::route};
+
+mod site;
+mod html;
+
+type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
+
+pub struct Page {
+ title: String,
+ html: Element,
+}
+
+pub struct Request {
+ pub path: String,
+ pub query: String,
+}
+
+fn parse() -> Result<Request> {
+ 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!("<!DOCTYPE html>");
+ println!("{top}");
+
+ Ok(())
+}
blob - /dev/null
blob + 2ecb2164438d24a4fe5c5ca38fe1496a88a3339e (mode 644)
--- /dev/null
+++ src/site/cats.rs
+use crate::{site::Page, html, Result, Request};
+
+
+pub fn index(req: &Request) -> Result<Page> {
+ 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
+use crate::{Result, site::Page, html, Request};
+
+pub fn index(_req: &Request) -> Result<Page> {
+ 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
+use itertools::Itertools;
+use crate::{Result, Page, Request, html, html::Element};
+
+pub type Handler = fn(&Request) -> Result<Page>;
+
+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<Page> {
+ 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<Page> {
+ (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
+use chrono::Local;
+use crate::{Request, Result, Page, html};
+
+pub fn index(_req: &Request) -> Result<Page> {
+ 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
+a {
+ color: dodgerblue;
+}