about summary refs log tree commit diff
path: root/splorm/src/handler
diff options
context:
space:
mode:
authorgennyble <gen@nyble.dev>2026-09-21 14:23:28 -0500
committergennyble <gen@nyble.dev>2026-09-21 14:23:28 -0500
commitde0e60603d3e470befad49f2ec59778b5d1b5d59 (patch)
tree861edd0aaa1745e97cca926262e0eb73448da83c /splorm/src/handler
parentbe872aa0cd61ea16b95eddcd68c6a72f0fe3af96 (diff)
splorm, i wake and breathe
Diffstat (limited to 'splorm/src/handler')
-rw-r--r--splorm/src/handler/mod.rs25
-rw-r--r--splorm/src/handler/staticfile.rs52
-rw-r--r--splorm/src/handler/templated.rs131
3 files changed, 208 insertions, 0 deletions
diff --git a/splorm/src/handler/mod.rs b/splorm/src/handler/mod.rs
new file mode 100644
index 0000000..0089c48
--- /dev/null
+++ b/splorm/src/handler/mod.rs
@@ -0,0 +1,25 @@
+use axum::{Extension, extract::Path, response::Response};
+
+mod staticfile;
+mod templated;
+
+use crate::{RuntimeError, config::Settings, fs::RequestPath};
+
+pub async fn index(Extension(settings): Extension<Settings>) -> Result<Response, RuntimeError> {
+	println!("index!");
+	let rqp = RequestPath::verify_safe(settings.root(), "home.html")?;
+	templated::send_file(rqp, &settings).await
+}
+
+pub async fn static_path(
+	Extension(settings): Extension<Settings>,
+	Path(path): Path<String>,
+) -> Result<Response, RuntimeError> {
+	println!("{path}");
+	let rqp = RequestPath::verify_safe(settings.root(), &path)?;
+	if rqp.guess_mime().as_ref() == "text/html" {
+		templated::send_file(rqp, &settings).await
+	} else {
+		staticfile::send_or_stream_file(rqp).await
+	}
+}
diff --git a/splorm/src/handler/staticfile.rs b/splorm/src/handler/staticfile.rs
new file mode 100644
index 0000000..8f26251
--- /dev/null
+++ b/splorm/src/handler/staticfile.rs
@@ -0,0 +1,52 @@
+use axum::{body::Body, response::Response};
+use tokio_util::io::ReaderStream;
+
+use crate::{
+	RuntimeError,
+	fs::{self, Mimetype, RequestPath},
+};
+
+const STREAM_AFTER: u64 = 1024 * 1024 * 5;
+pub async fn send_or_stream_file(path: RequestPath) -> Result<Response, RuntimeError> {
+	let meta = path.system_path().metadata()?;
+
+	if meta.len() > STREAM_AFTER {
+		stream_file(path).await
+	} else {
+		send_file(path).await
+	}
+}
+
+pub async fn send_file(path: RequestPath) -> Result<Response, RuntimeError> {
+	let mime = path.guess_mime();
+
+	let body = match mime.mimetype() {
+		Mimetype::Text => {
+			let text = fs::read_file_to_string(path.system_path()).await?;
+			Body::from(text)
+		}
+		Mimetype::Media => {
+			let data = fs::read_file(path.system_path()).await?;
+			Body::from(data)
+		}
+	};
+
+	Response::builder()
+		.status(200)
+		.header("Content-Type", mime.as_ref())
+		.body(body)
+		.map_err(|_| RuntimeError::unknown())
+}
+
+pub async fn stream_file(path: RequestPath) -> Result<Response, RuntimeError> {
+	let mime = path.guess_mime();
+
+	let file = fs::open_file(path.system_path()).await?;
+	let stream = ReaderStream::new(file);
+
+	Response::builder()
+		.status(200)
+		.header("Content-Type", mime.as_ref())
+		.body(Body::from_stream(stream))
+		.map_err(|_| RuntimeError::unknown())
+}
diff --git a/splorm/src/handler/templated.rs b/splorm/src/handler/templated.rs
new file mode 100644
index 0000000..596210d
--- /dev/null
+++ b/splorm/src/handler/templated.rs
@@ -0,0 +1,131 @@
+use std::str::FromStr;
+
+use axum::{body::Body, response::Response};
+use bempline::{Document, Options, options::IncludeMethod, variables};
+use paige::{Frontmatter, Templated};
+use timeparse::OffsetDateTime;
+
+use crate::{
+	RuntimeError,
+	config::Settings,
+	fs::{self, RequestPath, SafePath, SafePathBuf},
+};
+
+pub async fn send_file(path: RequestPath, settings: &Settings) -> Result<Response, RuntimeError> {
+	let safe = path.system_path().to_owned();
+	let string = fs::read_file_to_string(path.system_path()).await?;
+	let template = Templated::from_str(&string)
+		.map_err(|_| RuntimeError::not_a_template(path.system_path().to_path_buf()))?;
+	let tvars = TemplateVariables::build(safe.as_path(), &template.frontmatter);
+
+	let pt_path = match tvars.template {
+		Some(name) => {
+			let path = RequestPath::verify_safe(settings.template_dir(), &format!("{name}.html"))?;
+			path.system_path().to_owned()
+		}
+		None => settings.default_template_path(),
+	};
+
+	let mut page_template = get_template(pt_path).await?;
+	tvars.apply_title(&mut page_template);
+	tvars.apply_styles(&mut page_template);
+	tvars.apply_published(&mut page_template);
+	tvars.apply_pathnav(&mut page_template, path);
+	page_template.set("main", template.content);
+
+	Response::builder()
+		.status(200)
+		.header("content-type", "text/html")
+		.body(Body::from(page_template.compile()))
+		.map_err(|_| RuntimeError::Unknown)
+}
+
+async fn get_template(path: SafePathBuf) -> Result<Document, RuntimeError> {
+	let options = Options::new().include_path(IncludeMethod::Path(path.as_std_path().to_owned()));
+	let str = fs::read_file_to_string(path.as_path()).await?;
+
+	Document::from_str(str, options)
+		.map_err(|perr| RuntimeError::template_parse_error(perr, path.to_path_buf()))
+}
+
+pub struct TemplateVariables<'fm> {
+	template: Option<&'fm str>,
+
+	style: Vec<&'fm str>,
+
+	title: &'fm str,
+	description: Option<&'fm str>,
+	published: Option<OffsetDateTime>,
+
+	skip_path: Option<usize>,
+}
+
+impl<'fm> TemplateVariables<'fm> {
+	pub fn build(safe: SafePath<'fm>, fm: &'fm Frontmatter) -> Self {
+		let filename = safe.file_name().unwrap_or("unknown");
+
+		Self {
+			template: fm.get("template"),
+			title: fm.get("title").unwrap_or_else(|| filename),
+			style: fm.get_many("style"),
+			description: fm.get("description"),
+			published: fm
+				.get("published")
+				.map(|raw| timeparse::parse(raw).ok())
+				.flatten(),
+			skip_path: fm.get("skip-path").map(|s| s.parse().ok()).flatten(),
+		}
+	}
+
+	pub fn apply_styles(&self, doc: &mut Document) {
+		if let Some(styles) = doc.get_pattern("styles") {
+			self.style.iter().for_each(|style| {
+				let mut cloned = styles.clone();
+				cloned.set("style", style);
+				doc.set_pattern(cloned);
+			});
+		}
+	}
+
+	pub fn apply_title(&self, doc: &mut Document) {
+		doc.set("title", self.title);
+	}
+
+	pub fn apply_published(&self, doc: &mut Document) {
+		if let Some(mut published_pattern) = doc.get_pattern("published") {
+			if let Some(published) = self.published.as_ref() {
+				let published_human = timeparse::format::human_long(published);
+				let published_machine = timeparse::format::iso8601(published);
+
+				variables!(published_pattern, published_human, published_machine);
+				doc.set_pattern(published_pattern);
+			}
+		}
+	}
+
+	pub fn apply_pathnav(&self, doc: &mut Document, path: RequestPath) {
+		let webpath = path.web_path();
+
+		let mut components: Vec<&str> = webpath.split('/').collect();
+		// we don't want the name itself
+		components.pop();
+
+		if let Some(path_pattern) = doc.get_pattern("pathnav") {
+			let mut home = path_pattern.clone();
+			home.set("pathnav_link", "/");
+			home.set("pathnav_name", "home");
+			doc.set_pattern(home);
+
+			let count = components.len().saturating_sub(self.skip_path.unwrap_or(0));
+			let mut link = String::from("/");
+			for part in components.iter().take(count) {
+				link.push_str(&format!("{part}/"));
+
+				let mut pat = path_pattern.clone();
+				pat.set("pathnav_link", &link);
+				pat.set("pathnav_name", part);
+				doc.set_pattern(pat);
+			}
+		}
+	}
+}