commit 4cd4f508c779119905ab9b9d2ac22c9b0fedab51 from: Benjamin Stürz date: Thu May 02 02:07:25 2024 UTC add /posts commit - 7aeb283ccfe023dbb0450aebc274f477136373a7 commit + 4cd4f508c779119905ab9b9d2ac22c9b0fedab51 blob - f5610c5363aaf340dd7b51649d04782260bf5d3b blob + c9a2bda3ca9874ab546594f77d3cd8648a9368d7 --- Cargo.lock +++ Cargo.lock @@ -3,6 +3,15 @@ version = 3 [[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -18,6 +27,12 @@ dependencies = [ ] [[package]] +name = "anyhow" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" + +[[package]] name = "autocfg" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -68,6 +83,17 @@ source = "registry+https://github.com/rust-lang/crates checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] +name = "getrandom" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] name = "iana-time-zone" version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -109,6 +135,12 @@ dependencies = [ ] [[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -121,6 +153,12 @@ source = "registry+https://github.com/rust-lang/crates checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] name = "num-traits" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -136,6 +174,21 @@ source = "registry+https://github.com/rust-lang/crates checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] +name = "pledge" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252599417b7d9a43b7fdc63dd790b0848666a8910b2ebe1a25118309c3c981e5" +dependencies = [ + "libc", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] name = "proc-macro2" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -154,6 +207,65 @@ dependencies = [ ] [[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + +[[package]] name = "syn" version = "2.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -171,6 +283,21 @@ source = "registry+https://github.com/rust-lang/crates checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] +name = "unveil" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e7fa867d559102001ec694165ed17d5f82e95213060a65f9c8b6280084bbfec" +dependencies = [ + "libc", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] name = "wasm-bindgen" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -294,6 +421,12 @@ checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16 name = "www-cgi" version = "0.1.0" dependencies = [ + "anyhow", "chrono", "itertools", + "lazy_static", + "pledge", + "rand", + "regex", + "unveil", ] blob - edf52e3f33ca9a1b86120d49ec312c3d8221feb4 blob + b9b934e4170c8bf0a4cb39c1f5b644171ad907e3 --- Cargo.toml +++ Cargo.toml @@ -6,5 +6,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = "1.0.81" chrono = "0.4.37" itertools = "0.12.1" +lazy_static = "1.4.0" +pledge = "0.4.2" +rand = "0.8.5" +regex = "1.10.4" +unveil = "0.3.2" blob - 66a34507a8648c546ed689da668190936ab6ce2a blob + 8becc8c6c5bbe033a3f1b955002b764e8ba23592 --- Makefile +++ Makefile @@ -16,8 +16,8 @@ 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/ + ssh server "cd /var/www/bin && cat - > www.cgi.new && chmod 755 www.cgi.new && mv www.cgi.new www.cgi" < www.cgi + openrsync --rsync-path=/usr/bin/openrsync -tr --delete --exclude fix www-cgi server:/var/www/htdocs/ www.cgi: ${SRC} src/style.css cargo rustc --release -- ${RUSTFLAGS} blob - 7bedcdc0bf2a3c12294b2082d086c7a6697774e3 (mode 644) blob + /dev/null Binary files cats/1.jpg and /dev/null differ blob - a79e1b8932a75749300d618aa171a0b6c7625a7d (mode 644) blob + /dev/null Binary files cats/10.jpg and /dev/null differ blob - b43f918169ce12d67f1cdc0d44a1b3689f7fdedb (mode 644) blob + /dev/null Binary files cats/11.jpg and /dev/null differ blob - 4998b5f31b54224588e6ee9d5974579c40a40d36 (mode 644) blob + /dev/null Binary files cats/12.jpg and /dev/null differ blob - cf7ba488e8766077d7ef10110ca61ee025958c55 (mode 644) blob + /dev/null Binary files cats/13.jpg and /dev/null differ blob - e4953de7b4091c16e7894961daf2bce4b0f555e0 (mode 644) blob + /dev/null Binary files cats/14.jpg and /dev/null differ blob - 9574078edfb79338cc2caba1ed8da6ddf40b6924 (mode 644) blob + /dev/null Binary files cats/15.jpg and /dev/null differ blob - 323579dd34f4be587b146451ea83b6dc6795e17c (mode 644) blob + /dev/null Binary files cats/16.jpg and /dev/null differ blob - 44d2bbaa1d866c126595a702ffd1e9c14fc60266 (mode 644) blob + /dev/null Binary files cats/17.jpg and /dev/null differ blob - 2bea3e4213191917739baa4a2d32cfb91aa77693 (mode 644) blob + /dev/null Binary files cats/18.jpg and /dev/null differ blob - dae59c6e7706d225d073c4cf7b5a0cf440a79c90 (mode 644) blob + /dev/null Binary files cats/19.jpg and /dev/null differ blob - 8375a0240277de48131bef446d7959b128d1fbf1 (mode 644) blob + /dev/null Binary files cats/2.jpg and /dev/null differ blob - b96e4ba3518b475dfb8a79259ea7ae9b6cbb46c1 (mode 644) blob + /dev/null Binary files cats/20.jpg and /dev/null differ blob - 376527b16c4236a21db689c093f99ccd69735e8f (mode 644) blob + /dev/null Binary files cats/21.jpg and /dev/null differ blob - bb269d0a6b70ff58bbca3725fb11af3b75acc093 (mode 644) blob + /dev/null Binary files cats/22.jpg and /dev/null differ blob - 50930ac894fcc6eaaa24e0d53cc38a7765df4128 (mode 644) blob + /dev/null Binary files cats/23.jpg and /dev/null differ blob - 44720e2cea2e70ca2b5ed8fd8edafc38b29eb8d8 (mode 644) blob + /dev/null Binary files cats/24.jpg and /dev/null differ blob - 5a76b8cffec005fe05721a6e8085e223814c4c41 (mode 644) blob + /dev/null Binary files cats/25.jpg and /dev/null differ blob - 9410cad4d5034dc88b56eddff546cf8d443de7e0 (mode 644) blob + /dev/null Binary files cats/26.jpg and /dev/null differ blob - bdd3117974a1753c25328db71767e62da730af6e (mode 644) blob + /dev/null Binary files cats/27.jpg and /dev/null differ blob - f870d288d92e6d96ecab53902d0d40654a31ad1a (mode 644) blob + /dev/null Binary files cats/28.jpg and /dev/null differ blob - 220d0e4135a019f04d308cdaba8d63429f9643cb (mode 644) blob + /dev/null Binary files cats/3.jpg and /dev/null differ blob - eb672b6b24d6368995be0cbf5f17411867799ba7 (mode 644) blob + /dev/null Binary files cats/4.jpg and /dev/null differ blob - c4882f6eef556b98b614c1b80b93ea797c80989d (mode 644) blob + /dev/null Binary files cats/5.jpg and /dev/null differ blob - 82c9d90af5cf146e0ace8fc3a1d84f4baa0c9d20 (mode 644) blob + /dev/null Binary files cats/6.jpg and /dev/null differ blob - f604acadf017b7a32322742608336ad5b13201d9 (mode 644) blob + /dev/null Binary files cats/7.jpg and /dev/null differ blob - d61e8544fe298c2f82aab8de4c4bbb81b1d13ada (mode 644) blob + /dev/null Binary files cats/8.jpg and /dev/null differ blob - 08bfbfab54d01d524ee06f8c03fb43c5720752ab (mode 644) blob + /dev/null Binary files cats/9.jpg and /dev/null differ blob - 9553ae49e957dc45fe0ab1dfe070911fd07c60e1 (mode 755) blob + /dev/null --- cats/fix +++ /dev/null @@ -1,27 +0,0 @@ -#!/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 - 631940df13cd97176e7206e4544b1c19dae1eae5 blob + 2d337971565404597d6b3f9384a996161605ef14 --- src/html.rs +++ src/html.rs @@ -1,6 +1,6 @@ use std::fmt::{self, Display, Formatter}; -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct Tag { pub name: &'static str, pub args: Vec<(&'static str, String)>, @@ -8,7 +8,7 @@ pub struct Tag { pub self_closing: bool, } -#[derive(Clone)] +#[derive(Debug, Clone)] pub enum Element { Tag(Tag), String(String), @@ -17,6 +17,17 @@ pub enum Element { #[derive(Clone, Copy)] pub struct Indent(u32); +impl Tag { + pub fn new(name: &'static str) -> Self { + Self { + name, + args: Vec::new(), + children: Vec::new(), + self_closing: false, + } + } +} + impl Element { pub fn render(&self, f: &mut Formatter<'_>, ind: Indent) -> fmt::Result { match self { @@ -42,6 +53,7 @@ impl Element { } } }, + Self::String(s) if s.is_empty() => Ok(()), Self::String(s) => writeln!(f, "{ind}{s}"), } } blob - f160afdc57b27872393d3c3419d1bd1ce8f4c135 blob + cb97d409ea077b2cfb38f78544d4b3afc41f3298 --- src/main.rs +++ src/main.rs @@ -1,9 +1,14 @@ +pub use anyhow::Result; +use pledge::pledge; +use unveil::unveil; use crate::{html::Element, site::route}; +const CONFIG_BASE_DIR: &str = "/htdocs/www-cgi"; + mod site; mod html; +mod markdown; -type Result> = std::result::Result; pub struct Page { title: String, @@ -15,6 +20,15 @@ pub struct Request { pub query: String, } +impl Element { + fn as_page(self, title: impl Into) -> Page { + Page { + title: title.into(), + html: self, + } + } +} + fn parse() -> Result { let (path, query) = if std::env::args().count() >= 2 { let path = std::env::args().nth(1).unwrap(); @@ -23,6 +37,11 @@ fn parse() -> Result { } else { let path = std::env::var("DOCUMENT_URI")?; let query = std::env::var("QUERY_STRING")?; + + unveil(CONFIG_BASE_DIR, "r")?; + unveil("", "")?; + pledge("stdio rpath", None)?; + (path, query) }; @@ -59,15 +78,17 @@ fn main() -> Result<()> { }; let top = html! { - html { + html [lang="en"] { head { - title = format!("Test - {}", page.title); + title = format!("{} - Test", page.title); + meta [charset="utf-8"]; style { [ include_str!("style.css") .lines() ] } + //meta ["http-equiv"="Content-Security-Policy", content="default"] {} } body [style="background-color: #222; color: #ccc; font-size: 18px"] { {site::menu()} @@ -80,6 +101,7 @@ fn main() -> Result<()> { println!("Status: 500 Internal Server Error"); } println!("Content-Type: text/html"); + println!("X-Frame-Options: ALLOW-FROM *"); println!(); println!(""); println!("{top}"); blob - 2ecb2164438d24a4fe5c5ca38fe1496a88a3339e blob + d042dbc2f78261480b0cb1c5ab414f548bc93487 --- src/site/cats.rs +++ src/site/cats.rs @@ -1,11 +1,11 @@ -use crate::{site::Page, html, Result, Request}; +use crate::{site::Page, html, Result, Request, CONFIG_BASE_DIR}; +use rand::Rng; - pub fn index(req: &Request) -> Result { let mut max = 0u32; let q = &req.query; - for ent in std::fs::read_dir("/htdocs/cats")? { + for ent in std::fs::read_dir(format!("{CONFIG_BASE_DIR}/cats"))? { let ent = ent?; let name = ent.file_name(); let name = name.to_string_lossy(); @@ -42,10 +42,13 @@ pub fn index(req: &Request) -> Result { } }; + let r = rand::thread_rng() + .gen_range(1..=max); + 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"] { + main [style="width: 1280px; margin-left: auto; margin-right: auto; width: fit-content"] { + h1 [align="center"] { "Cats"; } + div [style="border: 1px solid #fff; margin-left: auto; margin-right: auto; 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"]; } @@ -55,6 +58,8 @@ pub fn index(req: &Request) -> Result { " | "; {maybe_link(id > 1, "Prev", format!(".?{}", id - 1))} " | "; + {maybe_link(true, "Random", format!(".?{}", r))} + " | "; {maybe_link(id < max, "Next", format!(".?{}", id + 1))} " | "; {maybe_link(id < max, "Last", format!(".?{}", max))} blob - e0bab8843979f0282e188af477668371bf19f95f blob + 0c84777685acf7ca9bf0482222b0246dd97ce5f0 --- src/site/mod.rs +++ src/site/mod.rs @@ -12,6 +12,7 @@ struct Route { mod index; mod cats; mod time; +mod posts; macro_rules! routes { [$($prefix:literal => $handler:path $([$($label:tt)+])?),* $(,)?] => { @@ -53,13 +54,14 @@ fn not_found(req: &Request) -> Result { const ROUTES: &[Route] = routes![ "/time/" => time::index ["Time" : 2], "/cats/" => cats::index ["Cats" : 1], + "/posts/*" => posts::posts ["Posts" : 3], "/" => index::index ["Index" : 0], "/*" => not_found, ]; fn matches(path: &str, pattern: &str) -> bool { if pattern.ends_with("/*") { - path.ends_with(&pattern[..pattern.len() - 2]) + path.starts_with(&pattern[..pattern.len() - 1]) } else { path == pattern } blob - 3f0d74e31198ba166fb4cbb438549557642ba6ce blob + 62feb4253c0a8f7b27fb49558b2097a6e6cd7e1e --- src/site/time.rs +++ src/site/time.rs @@ -3,19 +3,13 @@ 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}"; } + head { + meta ["http-equiv"="refresh", content="3"] {} + } h1 = "The Current System Time"; p { "{time}"; } } blob - /dev/null blob + 5098b96832aeedc5de31179c607cb50dea1646a4 (mode 644) --- /dev/null +++ src/site/posts.rs @@ -0,0 +1,96 @@ +use anyhow::Result; +use crate::{Request, Page, html, CONFIG_BASE_DIR}; + +pub fn posts(req: &Request) -> Result { + let path = req + .path + .strip_prefix("/posts") + .unwrap() + .trim_end_matches('/'); + + + if path.is_empty() { + index() + } else { + render(path) + } +} + + +fn index() -> Result { + let mut entries = Vec::new(); + for ent in std::fs::read_dir(format!("{CONFIG_BASE_DIR}/posts"))? { + let ent = ent?; + let name = ent.file_name(); + let name = name.to_string_lossy(); + if name.starts_with('.') { + continue; + } + let Some(name) = name.strip_suffix(".md") else { continue }; + let meta = std::fs::read_to_string(ent.path()) + .map(|meta| crate::markdown::metadata(&meta)) + .unwrap_or_default(); + entries.push((name.to_string(), meta)); + } + + Ok(Page { + title: format!("Posts"), + html: html! { + main { + h1 = "Posts"; + ul { + [ + entries + .into_iter() + .map(|(name, meta)| { + let title = meta + .title + .as_deref() + .unwrap_or(&name); + html! { + li { + a [href=format!("/test/posts/{name}/")] { "{title}"; } + } + } + }) + ] + } + } + }, + }) +} + +fn render(path: &str) -> Result { + let filepath = format!("{CONFIG_BASE_DIR}/posts/{path}.md"); + let s = std::fs::read_to_string(filepath)?; + let md = crate::markdown::compile(&s)?; + let title = md.meta.title.as_deref().unwrap_or(path); + Ok(Page { + title: format!("{title} - Posts"), + html: html! { + main { + [ + md + .meta + .title + .as_deref() + .map(|title| html! { h1 { {title} } }) + .into_iter() + ] + {md.body} + [ + md + .meta + .date + .map(|date| html! { + p [id="markdown-date"] { + "Last updated on "; + {date.to_string() + "."} + } + }) + .into_iter() + ] + } + }, + }) +} blob - /dev/null blob + 7ac3223d79784ddca07460b7e33f4abbefee2f91 (mode 644) --- /dev/null +++ src/markdown.rs @@ -0,0 +1,334 @@ +use std::{iter::Peekable, str::Lines}; + +use anyhow::Result; +use chrono::NaiveDate; +use lazy_static::lazy_static; +use regex::Regex; +use crate::html::{Element, Tag}; + +#[derive(Default)] +pub struct Metadata { + pub title: Option, + pub date: Option, +} + +pub struct Markdown { + pub meta: Metadata, + pub body: Element, +} + +#[derive(PartialEq, Eq)] +enum Mode { + Normal, + Paragraph, + Code, + List, + Todo, +} + +struct Compiler { + stack: Vec, + mode: Mode, + meta: Metadata, +} + +fn escape_char(s: &mut String, ch: char) { + match ch { + '<' => s.push_str("<"), + '>' => s.push_str(">"), + '&' => s.push_str("&"), + '"' => s.push_str("""), + '\'' => s.push_str("'"), + _ => s.push(ch), + } +} +fn escape(text: &str) -> String { + let mut s = String::with_capacity(text.len()); + for ch in text.chars() { + escape_char(&mut s, ch); + } + s +} + +macro_rules! regex { + ($s:literal) => { + Regex::new($s) + .expect(concat!("failed to compile regex: ", $s)) + }; +} + +lazy_static! { + static ref REGEX_TODO_BEGIN: Regex = regex!(r"^-\s*\[( |x)\]\s+(.*)$"); +} + +impl Compiler { + fn new() -> Self { + let mut div = Tag::new("div"); + div.args.push(("class", "markdown".into())); + Self { + stack: vec![div], + mode: Mode::Normal, + meta: Metadata::default(), + } + } + fn last(&self) -> &Tag { + self.stack.last().unwrap() + } + fn last_mut(&mut self) -> &mut Tag { + self.stack.last_mut().unwrap() + } + fn pop(&mut self) { + let tag = self.stack.pop().unwrap(); + self.last_mut().children.push(Element::Tag(tag)); + } + fn seq(&mut self, tag: &'static str) { + let last = self.last().name; + if last == tag { + self.pop(); + } else { + self.stack.push(Tag::new(tag)); + } + } + fn compile_string(&mut self, text: &str) { + let mut iter = text.chars().peekable(); + let num = self.stack.len(); + + while let Some(ch) = iter.next() { + let is_code = self.last().name == "span"; + if ch == '*' && !is_code { + let ty = if iter.next_if_eq(&'*').is_some() { + "b" + } else { + "i" + }; + self.seq(ty); + } else if ch == '_' && !is_code { + self.seq("u"); + } else if ch == '~' && !is_code { + self.seq("s"); + } else if ch == '`' { + self.seq("span"); + } else if ch == '[' && !is_code { + let mut text = String::new(); + while let Some(ch) = iter.next() { + if ch == ']' { + break; + } + text.push(ch); + } + if iter.next_if_eq(&'(').is_none() { + continue; + } + + let mut link = String::new(); + while let Some(ch) = iter.next() { + if ch == ')' { + break; + } + link.push(ch); + } + + let mut tag = Tag::new("a"); + tag.args.push(("href", escape(&link))); + self.stack.push(tag); + self.compile_string(&text); + self.pop(); + } else { + let last = self.last_mut(); + match last.children.last_mut() { + Some(Element::String(s)) => escape_char(s, ch), + _ => last.children.push(Element::String({ + let mut s = String::new(); + escape_char(&mut s, ch); + s + })), + } + } + } + + if self.stack.len() < num { + panic!("stack underflow"); + } else if self.stack.len() > num { + eprintln!("Warning: unclosed sequence"); + self.stack.resize_with(num, || unreachable!()); + } + } + fn set_mode(&mut self, m: Mode) { + if self.mode != Mode::Normal { + self.pop(); + } + self.mode = m; + } + fn push(&mut self, e: Element) { + self.last_mut().children.push(e); + } + fn compile_line(&mut self, line: &str) { + if self.mode == Mode::Code { + if line.starts_with("```") { + self.set_mode(Mode::Normal); + } else { + let mut tag = Tag::new("span"); + tag.children.push(Element::String(escape(line))); + self.push(Element::Tag(tag)); + let mut br = Tag::new("br"); + br.self_closing = true; + self.push(Element::Tag(br)); + } + return; + } + + let mut header = |prefix: &str, tag| { + self.set_mode(Mode::Normal); + self.stack.push(Tag::new(tag)); + let text = line + .trim_start_matches(prefix) + .trim(); + self.compile_string(text); + self.pop(); + }; + + if line.starts_with("####") { + header("####", "h4"); + } else if line.starts_with("###") { + header("###", "h3"); + } else if line.starts_with("##") { + header("##", "h2"); + } else if line.starts_with("#") { + header("#", "h1"); + } else if line.starts_with("```") { + self.set_mode(Mode::Code); + let mut tag = Tag::new("div"); + tag.args.push(("class", "code".into())); + self.stack.push(tag); + } else if let Some(cap) = REGEX_TODO_BEGIN.captures(line) { + let rest = cap.get(2).unwrap().as_str(); + + let status = match cap.get(1).unwrap().as_str() { + "x" => "done", + " " => "open", + s => panic!("invalid todo: {s}"), + }; + + if self.mode != Mode::Todo { + self.set_mode(Mode::Todo); + self.stack.push(Tag { + name: "ul", + args: vec![("class", "todo".into())], + children: Vec::new(), + self_closing: false, + }); + } + + self.stack.push(Tag { + name: "li", + args: vec![("class", status.into())], + children: Vec::new(), + self_closing: false, + }); + + self.compile_string(rest); + + self.pop(); + } else if line.starts_with("-") { + let rest = line[1..].trim(); + + if self.mode != Mode::List { + self.set_mode(Mode::List); + self.stack.push(Tag { + name: "ul", + args: Vec::new(), + children: Vec::new(), + self_closing: false, + }); + } + + self.stack.push(Tag { + name: "li", + args: Vec::new(), + children: Vec::new(), + self_closing: false, + }); + + // TODO: TODO List + self.compile_string(rest); + + self.pop(); + } else if line.trim().is_empty() { + self.set_mode(Mode::Normal); + } else { + if self.mode != Mode::Paragraph { + self.set_mode(Mode::Paragraph); + self.stack.push(Tag::new("p")); + } else { + self.push(Element::String(String::new())); + } + + let (line, br) = line.trim().strip_suffix('\\').map_or((line, false), |l| (l, true)); + self.compile_string(line); + if br { + let mut tag = Tag::new("br"); + tag.self_closing = true; + self.push(Element::Tag(tag)); + } + } + } + fn compile_metadata(it: &mut Peekable) -> Metadata { + let mut meta = Metadata::default(); + + while it.next_if(|s| s.trim().is_empty()).is_some() {} + + if it.next_if(|s| s.trim() == "---").is_some() { + while let Some(s) = it.next() { + let s = s.trim(); + if s == "---" { + break; + } + + let Some((name, value)) = s.split_once(':') else { continue }; + let name = name.trim(); + let value = value.trim(); + + match name { + "title" => meta.title = Some(value.into()), + "date" => meta.date = NaiveDate::parse_from_str(value, "%Y-%m-%d").ok(), + _ => {}, + } + } + } + + meta + } + fn compile(&mut self, text: &str) { + let mut it = text.lines().peekable(); + + self.meta = Self::compile_metadata(&mut it); + + for line in it { + self.compile_line(line); + } + } +} + +pub fn compile(text: &str) -> Result { + let mut compiler = Compiler::new(); + + compiler.compile(text); + + compiler.set_mode(Mode::Normal); + + if compiler.stack.len() != 1 { + dbg!(compiler.stack); + panic!("failed to compile markdown"); + } + + let md = Markdown { + body: Element::Tag(compiler.stack.pop().unwrap()), + meta: compiler.meta, + }; + Ok(md) +} + +pub fn metadata(text: &str) -> Metadata { + let mut it = text.lines().peekable(); + Compiler::compile_metadata(&mut it) +} blob - d94c14ec7f2b62b5fe3b73ee78cf4d02798472af blob + 4029789b69a343a46cdd3b6d63b192cafe5415b3 --- src/style.css +++ src/style.css @@ -1,3 +1,17 @@ a { color: dodgerblue; } + +.markdown .todo .open { + color: red; +} + + +.markdown .todo .done { + color: green; +} + +#markdown-date { + font-size: 0.8rem; + color: gray; +} blob - /dev/null blob + 7bedcdc0bf2a3c12294b2082d086c7a6697774e3 (mode 644) Binary files /dev/null and www-cgi/cats/1.jpg differ blob - /dev/null blob + a79e1b8932a75749300d618aa171a0b6c7625a7d (mode 644) Binary files /dev/null and www-cgi/cats/10.jpg differ blob - /dev/null blob + b43f918169ce12d67f1cdc0d44a1b3689f7fdedb (mode 644) Binary files /dev/null and www-cgi/cats/11.jpg differ blob - /dev/null blob + 4998b5f31b54224588e6ee9d5974579c40a40d36 (mode 644) Binary files /dev/null and www-cgi/cats/12.jpg differ blob - /dev/null blob + cf7ba488e8766077d7ef10110ca61ee025958c55 (mode 644) Binary files /dev/null and www-cgi/cats/13.jpg differ blob - /dev/null blob + e4953de7b4091c16e7894961daf2bce4b0f555e0 (mode 644) Binary files /dev/null and www-cgi/cats/14.jpg differ blob - /dev/null blob + 9574078edfb79338cc2caba1ed8da6ddf40b6924 (mode 644) Binary files /dev/null and www-cgi/cats/15.jpg differ blob - /dev/null blob + 323579dd34f4be587b146451ea83b6dc6795e17c (mode 644) Binary files /dev/null and www-cgi/cats/16.jpg differ blob - /dev/null blob + 44d2bbaa1d866c126595a702ffd1e9c14fc60266 (mode 644) Binary files /dev/null and www-cgi/cats/17.jpg differ blob - /dev/null blob + 2bea3e4213191917739baa4a2d32cfb91aa77693 (mode 644) Binary files /dev/null and www-cgi/cats/18.jpg differ blob - /dev/null blob + dae59c6e7706d225d073c4cf7b5a0cf440a79c90 (mode 644) Binary files /dev/null and www-cgi/cats/19.jpg differ blob - /dev/null blob + 8375a0240277de48131bef446d7959b128d1fbf1 (mode 644) Binary files /dev/null and www-cgi/cats/2.jpg differ blob - /dev/null blob + b96e4ba3518b475dfb8a79259ea7ae9b6cbb46c1 (mode 644) Binary files /dev/null and www-cgi/cats/20.jpg differ blob - /dev/null blob + 376527b16c4236a21db689c093f99ccd69735e8f (mode 644) Binary files /dev/null and www-cgi/cats/21.jpg differ blob - /dev/null blob + bb269d0a6b70ff58bbca3725fb11af3b75acc093 (mode 644) Binary files /dev/null and www-cgi/cats/22.jpg differ blob - /dev/null blob + 50930ac894fcc6eaaa24e0d53cc38a7765df4128 (mode 644) Binary files /dev/null and www-cgi/cats/23.jpg differ blob - /dev/null blob + 44720e2cea2e70ca2b5ed8fd8edafc38b29eb8d8 (mode 644) Binary files /dev/null and www-cgi/cats/24.jpg differ blob - /dev/null blob + 5a76b8cffec005fe05721a6e8085e223814c4c41 (mode 644) Binary files /dev/null and www-cgi/cats/25.jpg differ blob - /dev/null blob + 9410cad4d5034dc88b56eddff546cf8d443de7e0 (mode 644) Binary files /dev/null and www-cgi/cats/26.jpg differ blob - /dev/null blob + bdd3117974a1753c25328db71767e62da730af6e (mode 644) Binary files /dev/null and www-cgi/cats/27.jpg differ blob - /dev/null blob + f870d288d92e6d96ecab53902d0d40654a31ad1a (mode 644) Binary files /dev/null and www-cgi/cats/28.jpg differ blob - /dev/null blob + 220d0e4135a019f04d308cdaba8d63429f9643cb (mode 644) Binary files /dev/null and www-cgi/cats/3.jpg differ blob - /dev/null blob + eb672b6b24d6368995be0cbf5f17411867799ba7 (mode 644) Binary files /dev/null and www-cgi/cats/4.jpg differ blob - /dev/null blob + c4882f6eef556b98b614c1b80b93ea797c80989d (mode 644) Binary files /dev/null and www-cgi/cats/5.jpg differ blob - /dev/null blob + 82c9d90af5cf146e0ace8fc3a1d84f4baa0c9d20 (mode 644) Binary files /dev/null and www-cgi/cats/6.jpg differ blob - /dev/null blob + f604acadf017b7a32322742608336ad5b13201d9 (mode 644) Binary files /dev/null and www-cgi/cats/7.jpg differ blob - /dev/null blob + d61e8544fe298c2f82aab8de4c4bbb81b1d13ada (mode 644) Binary files /dev/null and www-cgi/cats/8.jpg differ blob - /dev/null blob + 08bfbfab54d01d524ee06f8c03fb43c5720752ab (mode 644) Binary files /dev/null and www-cgi/cats/9.jpg differ blob - /dev/null blob + 9553ae49e957dc45fe0ab1dfe070911fd07c60e1 (mode 755) --- /dev/null +++ www-cgi/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 + 3eb70d1d2df8aeabbbb94e30b7e81dcee593d574 (mode 644) --- /dev/null +++ www-cgi/posts/projects.md @@ -0,0 +1,29 @@ +--- +title: Benjamin Stürz' project list +date: 2024-05-02 +--- + +# My Projects List + +- [read-only FUSE driver for UFS](https://got.stuerz.xyz/?action=summary&path=fuse-ufs.git) (GSoC 2024) +- [64-Bit Linux Userspace RISC-V Emulator](https://got.stuerz.xyz/?action=summary&path=linurv.git) (2024) +- [16-Bit custom CPU written in Verilog](https://got.stuerz.xyz/?action=summary&path=cpu.git) (2024) +- [procfs FUSE implementation for OpenBSD](https://got.stuerz.xyz/?action=summary&path=procfs-fuse.git) (2024) +- [This CGI script](https://got.stuerz.xyz/?action=summary&path=www-cgi.git) (2024) +- [Minimal implementation of make(1) written in awk(1)](https://got.stuerz.xyz/?action=summary&path=junk.git) (2024) +- [Rust Accounting Solution](https://got.stuerz.xyz/?action=summary&path=accounthing2.git) (2023-2024) +- [Clone of lsblk(8) for OpenBSD](https://got.stuerz.xyz/?action=summary&path=lsblk.git) (2023) +- [C++ std::format-like standalone header library](https://github.com/riscygeek/safmatlib) (2022) +- [read-only tarfs FUSE implementation](https://github.com/riscygeek/fuse-tar) (2021) +- [Minimal implementation of POSIX userspace for Linux](https://github.com/riscygeek/microcoreutils) (2021) +- [Brainlet C Compiler](https://github.com/riscygeek/bcc) (2021-2022) +- [32-Bit RISC-V CPU written in Verilog](https://github.com/riscygeek/rv32-cpu) (2020-2021) +- [Compiler for a B-like language targeting i386-linux](https://github.com/riscygeek/benc) (2020) +- [Ben Eater's 8-Bit CPU written in Verilog](https://github.com/riscygeek/eater-8bit) (2020) +- [Assembler & VM for the Cosmos CP1](https://github.com/riscygeek/Cosmos-CP1) (2020) +- [My first virtual machine for an imaginary CPU architecture](https://github.com/riscygeek/MicroVM-8) (2018) +- [Minecraft Spigot Plugin for automatic tool legeling](https://github.com/riscygeek/ToolLeveling) (2017) +- [My first attempt at creating a programming language](https://github.com/riscygeek/CAlpha) (2017) +- Fake Windows XP written in Lua for the [Computercraft](https://www.computercraft.info/) Minecraft mod (lost, before 2017) +- [Multiple](https://github.com/riscygeek/BenOS) [attempts](https://github.com/riscygeek/nameless-os) [to](https://github.com/riscygeek/micro-linux) [create](https://gitlab.com/overlay-linux) an operating system (incomplete) +- Many more interesting projects, that I unfortunately lost over time... blob - /dev/null blob + e8bc60d441fd8e0eb85dd19340bcdc57a9e271d3 (mode 644) --- /dev/null +++ www-cgi/posts/test.md @@ -0,0 +1,68 @@ +--- +title: Example Markdown Document +date: 2024-05-02 +--- + +# Hello World + +This is a test markdown document. +[Link to index](/test/) + +This is some text \ +in a new paragraph. \ +The lines are split with the `/` character. + +This is a normal paragraph. +The sentences are not split across lines, +eventhough each sentence appears on it's own line. + +## Second heading + +### Third heading +*italic* +**bold** +_underlined_ +`monospace` +``` +monospace +code block +with +multiple +lines +``` + +### List +- A +- B +- C +- D +- E +- F + +# Supported features + +- [x] Paragraphs +- [x] Headings +- [x] **bold** text +- [x] *italic* text +- [x] _underlined_ text +- [x] ~strokethrough~ text +- [x] `monospace` text +- [x] Line splitting with the `/` character +- [x] Code blocks +- [x] Links +- [x] Escaping HTML sequences: `<>&` +- [x] Lists +- [x] TODO Lists +- [x] Metadata +- [ ] Images +- [ ] Tables +- [ ] Syntax Highlighting in Code Blocks with [tree-sitter](https://github.com/tree-sitter/tree-sitter-rust) + +# Metadata Format +``` +--- +title: Example Title +date: 2024-05-02 +--- +```