From fafb3c3ceaa16e93f69b38e33e64e240573e4875 Mon Sep 17 00:00:00 2001 From: gennyble Date: Sat, 13 Apr 2024 05:40:17 -0500 Subject: :) --- src/fs.rs | 3 +- src/ifc.rs | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 123 ++++++++++++++++++++++++++++++++++++++++++++++++---------- src/markup.rs | 15 ++++--- 4 files changed, 225 insertions(+), 28 deletions(-) create mode 100644 src/ifc.rs (limited to 'src') 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 { - 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 { - 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>(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 { 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 { 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 { async fn send_template( templated: Templated, - path: Utf8PathBuf, + resolve: PathResolution, + webpath: Webpath, settings: Settings, ) -> Result { 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::()) + { + 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 = "\\test"; + let correct = "

\ntest\n

"; + assert_eq!(process(str), correct) + } + const BASE: &str = "test/markup"; fn test_files(test: &str) { let input_path = format!("{BASE}/{test}/input.html"); -- cgit 1.4.1-3-g733a5