about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/fs.rs3
-rw-r--r--src/ifc.rs112
-rw-r--r--src/main.rs123
-rw-r--r--src/markup.rs15
4 files changed, 225 insertions, 28 deletions
diff --git a/src/fs.rs b/src/fs.rs
index b44e4c4..7dc94d3 100644
--- a/src/fs.rs
+++ b/src/fs.rs
@@ -8,7 +8,7 @@ use crate::RuntimeError;
 /// below the webroot and will never start or end with a slash.
 #[derive(Clone, Debug, PartialEq)]
 pub struct Webpath {
-	webcanon: Utf8PathBuf,
+	pub webcanon: Utf8PathBuf,
 	is_dir: bool,
 }
 
@@ -106,7 +106,6 @@ impl Filesystem {
 	}
 
 	pub fn resolve(&self, webpath: &Webpath) -> Result<PathResolution, RuntimeError> {
-		println!("resolve = {webpath}");
 		if webpath.is_index() || webpath == ROOT_INDEX {
 			return Ok(PathResolution {
 				filepath: self.webroot.join(ROOT_INDEX),
diff --git a/src/ifc.rs b/src/ifc.rs
new file mode 100644
index 0000000..d0a9f5f
--- /dev/null
+++ b/src/ifc.rs
@@ -0,0 +1,112 @@
+//! The international fixed calendar is a 13-month calendar with each month
+//! containing exactly 28 days. There is an extra day at the end of the year
+//! called the year day.
+//!
+//! In leap-years there is an extra day inserted at the end of June called the
+//! leap day. It is directly after the fourth week of june and is given to june,
+//! so it becomes June 29th. The day after June 29th starts the new month, Sol,
+//! with Sol 1.
+//!
+//! [Wikipedia: International Fixed Calendar][wp-ifc]
+//! [wp-ifc]: https://en.wikipedia.org/wiki/International_Fixed_Calendar
+
+use time::Date as TimeDate;
+
+const MONTHS: [[&str; 2]; 13] = [
+	["January", "Jan"],
+	["February", "Feb"],
+	["March", "Mar"],
+	["April", "Apr"],
+	["May", "May"],
+	["June", "Jun"],
+	["Sol", "Sol"],
+	["July", "Jul"],
+	["August", "Aug"],
+	["September", "Sep"],
+	["October", "Oct"],
+	["November", "Nov"],
+	["December", "Dec"],
+];
+
+pub struct Calendar {
+	pub year: usize,
+	pub ordinal: usize,
+}
+
+impl Calendar {
+	pub fn from_year(year: usize) -> Self {
+		Self { year, ordinal: 0 }
+	}
+
+	pub fn from_time_date(date: TimeDate) -> Self {
+		let year = date.year() as usize;
+		let ord = date.ordinal() as usize;
+
+		Self { year, ordinal: ord }
+	}
+}
+
+pub struct Date {
+	pub year: u32,
+	pub month: u8,
+	pub day: u8,
+}
+
+impl Date {
+	pub fn from_time_date(date: TimeDate) -> Self {
+		let year = date.year() as u32;
+		let ord = date.ordinal();
+
+		if !year_leaps(year) || ord <= 168 {
+			// not a leap year path
+			// also the "leap year but before the leap-day" path
+			Self {
+				year,
+				month: (ord / 28) as u8,
+				day: (ord % 28) as u8,
+			}
+		} else if ord == 169 {
+			Self {
+				year,
+				month: 6,
+				day: 29,
+			}
+		} else {
+			todo!()
+		}
+	}
+
+	pub fn is_leap(&self) -> bool {
+		year_leaps(self.year)
+	}
+}
+
+/// Whether or not a year is a leap year
+fn year_leaps(year: u32) -> bool {
+	let four = year % 4 == 0;
+	let hundreds = year % 100 == 0;
+	let fourhund = year % 400 == 0;
+
+	// leap if:
+	// - four AND NOT hundred
+	// - four AND hundred AND fourhund
+
+	// `fourhund` here checks `hundreds` by virtue of 100 being a multiple of 400
+	four && (!hundreds || fourhund)
+}
+
+mod test {
+	use crate::ifc::year_leaps;
+
+	#[test]
+	fn leap_years() {
+		// the examples given by wikipedia
+		assert!(year_leaps(2000));
+		assert!(!year_leaps(1700));
+		assert!(!year_leaps(1800));
+		assert!(!year_leaps(1900));
+
+		// testing the four rule
+		assert!(year_leaps(2024));
+	}
+}
diff --git a/src/main.rs b/src/main.rs
index 29716e8..b43e463 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,8 +1,11 @@
+mod atomizer;
 mod error;
 mod fs;
+mod ifc;
 mod markup;
 mod settings;
 mod templated;
+mod timeparse;
 
 use std::{os::unix::fs::MetadataExt, str::FromStr};
 
@@ -16,10 +19,12 @@ use axum::{
 };
 use bempline::{Document, Options};
 use camino::Utf8PathBuf;
+use confindent::Confindent;
 pub use error::RuntimeError;
 use fs::Filesystem;
 use settings::Settings;
 use tokio_util::io::ReaderStream;
+use tracing_subscriber::{prelude::*, EnvFilter};
 
 use crate::{
 	fs::{PathResolution, Webpath},
@@ -28,10 +33,35 @@ use crate::{
 
 #[tokio::main]
 async fn main() {
-	let fs = Filesystem::new("../inf/served");
+	match std::env::args().nth(1).as_deref() {
+		Some("atomizer") => atomizer::main(),
+		Some("serve") =>
+		/* fallthrough*/
+		{
+			()
+		}
+		_ => (),
+	}
+
+	tracing_subscriber::registry()
+		.with(tracing_subscriber::fmt::layer())
+		.with(
+			EnvFilter::try_from_default_env()
+				.or_else(|_| EnvFilter::try_new("info"))
+				.unwrap(),
+		)
+		.init();
+
+	let conf = Confindent::from_file(std::env::args().nth(2).unwrap()).unwrap();
+	let webroot: Utf8PathBuf = conf.child_parse("Webroot").unwrap();
+	let templates = conf.child_value("Templates").unwrap();
+
+	let fs = Filesystem::new(&webroot);
 
 	let settings = Settings {
-		template_dir: Utf8PathBuf::from("../inf/templates"),
+		template_dir: Utf8PathBuf::from(webroot.join(templates))
+			.canonicalize_utf8()
+			.unwrap(),
 	};
 
 	let app = Router::new()
@@ -66,30 +96,25 @@ async fn falible_handler(
 	settings: Settings,
 	path: String,
 ) -> Result<Response, RuntimeError> {
-	println!("raw = {path}");
+	tracing::debug!("webpath = {path}");
 
 	let webpath: Webpath = path.parse()?;
+	let resolve = fs.resolve(&webpath)?;
 
-	println!("path = {path}");
-
-	let PathResolution {
-		filepath,
-		is_dirfile,
-	} = fs.resolve(&webpath)?;
-
-	if !webpath.is_dir() && is_dirfile {
-		println!("as_dir = {}", webpath.as_dir());
+	if !webpath.is_dir() && resolve.is_dirfile {
 		return Ok(redirect(webpath.as_dir()));
 	}
 
-	let ext = filepath.extension().unwrap_or_default();
+	tracing::info!("serving {webpath}");
+
+	let ext = resolve.filepath.extension().unwrap_or_default();
 
 	if ext != "html" {
-		send_file(filepath).await
+		send_file(resolve.filepath).await
 	} else {
-		let content = Filesystem::read_to_string(&filepath).await?;
+		let content = Filesystem::read_to_string(&resolve.filepath).await?;
 		match Templated::from_str(&content) {
-			Ok(templated) => send_template(templated, filepath, settings).await,
+			Ok(templated) => send_template(templated, resolve, webpath, settings).await,
 			Err(_) => Ok(Response::builder()
 				.header(header::CONTENT_TYPE, "text/html")
 				.body(Body::from(content))
@@ -100,7 +125,7 @@ async fn falible_handler(
 
 fn redirect<S: Into<String>>(redirection: S) -> Response {
 	let location = redirection.into();
-	println!("redirecting to {location}");
+	tracing::info!("redirect to {location}");
 	Response::builder()
 		.status(StatusCode::TEMPORARY_REDIRECT)
 		.header(header::LOCATION, &location)
@@ -113,13 +138,16 @@ const STREAM_AFTER: u64 = 20 * 1024 * 1024;
 
 async fn send_file(filepath: Utf8PathBuf) -> Result<Response, RuntimeError> {
 	let ext = filepath.extension().unwrap_or_default();
+	let stem = filepath.file_stem().unwrap_or_default();
 
 	let mime = match ext {
 		// Text
 		"css" => "text/css",
 		"html" => "text/html",
 		"js" => "text/javascript",
-		"txt" => "txt/plain",
+		"txt" => "text/plain",
+		"xml" if stem.ends_with("atom") => "application/atom+xml",
+		"xml" => "application/xml",
 
 		// Multimedia
 		"gif" => "image/gif",
@@ -136,6 +164,8 @@ async fn send_file(filepath: Utf8PathBuf) -> Result<Response, RuntimeError> {
 
 	let metadata = Filesystem::metadata(&filepath)?;
 	if metadata.size() > STREAM_AFTER {
+		tracing::debug!("large file, streaming to client");
+
 		let file = Filesystem::open(filepath).await?;
 		let stream = ReaderStream::new(file);
 		Ok(response.body(Body::from_stream(stream)).unwrap())
@@ -147,14 +177,18 @@ async fn send_file(filepath: Utf8PathBuf) -> Result<Response, RuntimeError> {
 
 async fn send_template(
 	templated: Templated,
-	path: Utf8PathBuf,
+	resolve: PathResolution,
+	webpath: Webpath,
 	settings: Settings,
 ) -> Result<Response, RuntimeError> {
 	let template_stem = templated.frontmatter.get("template").expect("no template");
 	let template_name = Utf8PathBuf::from(format!("{template_stem}.html"));
 	let template_path = settings.template_dir.join(template_name);
 
-	let filename = path.file_name().expect("template has no filename");
+	let filename = resolve
+		.filepath
+		.file_name()
+		.expect("template has no filename");
 
 	let mut template = Document::from_file(
 		template_path,
@@ -169,6 +203,7 @@ async fn send_template(
 		templated.frontmatter.get("title").unwrap_or(filename),
 	);
 
+	// styles the templated stuff wants
 	let style_pattern = template.get_pattern("styles").unwrap();
 	for style in templated.frontmatter.get_many("style") {
 		let mut pat = style_pattern.clone();
@@ -176,7 +211,53 @@ async fn send_template(
 		template.set_pattern("styles", pat);
 	}
 
-	template.set("main", templated.content);
+	// path to the file for navigation
+	let mut path: Vec<&str> = webpath.webcanon.iter().collect();
+	// we don't want the directory/filename itself
+	path.pop();
+
+	if let Some(path_pattern) = template.get_pattern("path") {
+		let offset = match templated
+			.frontmatter
+			.get("path-offset")
+			.map(|raw| raw.parse::<usize>())
+		{
+			Some(Ok(offset)) => offset,
+			None => 0,
+			Some(Err(_)) => {
+				tracing::error!(
+					"path-offset in template {} is not an integer",
+					resolve.filepath
+				);
+
+				0
+			}
+		};
+
+		for _ in 0..offset {
+			path.pop();
+		}
+
+		let mut link = Utf8PathBuf::from("/");
+
+		let mut pat = path_pattern.clone();
+		pat.set("path_link", "/");
+		pat.set("path_name", "home");
+		template.set_pattern("path", pat);
+
+		for part in path {
+			link.push(part);
+
+			let mut pat = path_pattern.clone();
+			pat.set("path_link", &link);
+			pat.set("path_name", part);
+			template.set_pattern("path", pat);
+		}
+	}
+
+	// insert the page content itself
+	let markedup = markup::process(&templated.content);
+	template.set("main", markedup);
 
 	Ok(Response::builder()
 		.header(header::CONTENT_TYPE, "text/html")
diff --git a/src/markup.rs b/src/markup.rs
index 4e0d66e..a1516f4 100644
--- a/src/markup.rs
+++ b/src/markup.rs
@@ -47,12 +47,12 @@ impl State {
 
 			let escaped = self.escape_line(line);
 			self.current.push_str(escaped);
+			
+			self.last_blank = false;
 		} else {
 			// line is empty.
 			self.push_current();
 		}
-
-		self.last_blank = false;
 	}
 
 	pub fn done(mut self) -> String {
@@ -62,7 +62,7 @@ impl State {
 
 	fn escape_line<'a>(&mut self, line: &'a str) -> &'a str {
 		if let Some(strip) = line.strip_prefix('\\') {
-			match line.chars().next() {
+			match strip.chars().next() {
 				Some('[') => strip,
 				Some('<') => {
 					if self.last_blank {
@@ -157,8 +157,6 @@ pub fn process(raw: &str) -> String {
 
 #[cfg(test)]
 mod test {
-	use camino::Utf8PathBuf;
-
 	use crate::markup::process;
 
 	#[test]
@@ -200,6 +198,13 @@ mod test {
 		assert_eq!(process(str), correct)
 	}
 
+	#[test]
+	fn wraps_escaped_html() {
+		let str = "\\<i>test</i>";
+		let correct = "<p>\n<i>test</i>\n</p>";
+		assert_eq!(process(str), correct)
+	}
+
 	const BASE: &str = "test/markup";
 	fn test_files(test: &str) {
 		let input_path = format!("{BASE}/{test}/input.html");