commit - 7aeb283ccfe023dbb0450aebc274f477136373a7
commit + 4cd4f508c779119905ab9b9d2ac22c9b0fedab51
blob - f5610c5363aaf340dd7b51649d04782260bf5d3b
blob + c9a2bda3ca9874ab546594f77d3cd8648a9368d7
--- Cargo.lock
+++ Cargo.lock
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"
]
[[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"
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"
]
[[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"
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"
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"
]
[[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"
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"
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
# 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
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
-#!/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
use std::fmt::{self, Display, Formatter};
-#[derive(Clone)]
+#[derive(Debug, Clone)]
pub struct Tag {
pub name: &'static str,
pub args: Vec<(&'static str, String)>,
pub self_closing: bool,
}
-#[derive(Clone)]
+#[derive(Debug, Clone)]
pub enum Element {
Tag(Tag),
String(String),
#[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 {
}
}
},
+ 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
+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<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
pub struct Page {
title: String,
pub query: String,
}
+impl Element {
+ fn as_page(self, title: impl Into<String>) -> Page {
+ Page {
+ title: title.into(),
+ html: self,
+ }
+ }
+}
+
fn parse() -> Result<Request> {
let (path, query) = if std::env::args().count() >= 2 {
let path = std::env::args().nth(1).unwrap();
} 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)
};
};
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()}
println!("Status: 500 Internal Server Error");
}
println!("Content-Type: text/html");
+ println!("X-Frame-Options: ALLOW-FROM *");
println!();
println!("<!DOCTYPE html>");
println!("{top}");
blob - 2ecb2164438d24a4fe5c5ca38fe1496a88a3339e
blob + d042dbc2f78261480b0cb1c5ab414f548bc93487
--- src/site/cats.rs
+++ src/site/cats.rs
-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<Page> {
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();
}
};
+ 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"];
}
" | ";
{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
mod index;
mod cats;
mod time;
+mod posts;
macro_rules! routes {
[$($prefix:literal => $handler:path $([$($label:tt)+])?),* $(,)?] => {
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
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}"; }
+ 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
+use anyhow::Result;
+use crate::{Request, Page, html, CONFIG_BASE_DIR};
+
+pub fn posts(req: &Request) -> Result<Page> {
+ let path = req
+ .path
+ .strip_prefix("/posts")
+ .unwrap()
+ .trim_end_matches('/');
+
+
+ if path.is_empty() {
+ index()
+ } else {
+ render(path)
+ }
+}
+
+
+fn index() -> Result<Page> {
+ 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<Page> {
+ 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
+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<String>,
+ pub date: Option<NaiveDate>,
+}
+
+pub struct Markdown {
+ pub meta: Metadata,
+ pub body: Element,
+}
+
+#[derive(PartialEq, Eq)]
+enum Mode {
+ Normal,
+ Paragraph,
+ Code,
+ List,
+ Todo,
+}
+
+struct Compiler {
+ stack: Vec<Tag>,
+ 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<Lines>) -> 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<Markdown> {
+ 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
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
+#!/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
+---
+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
+---
+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
+---
+```