From d716ffb05f086061d9a5302f67f3695457fcff02 Mon Sep 17 00:00:00 2001 From: gennyble Date: Sat, 24 Feb 2024 02:24:50 -0600 Subject: Implement basic markup system this also has the redirection from non-trailing-slash-directories to their slashy counterparts --- src/main.rs | 42 +++++++++-- src/markup.rs | 218 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 256 insertions(+), 4 deletions(-) create mode 100644 src/markup.rs (limited to 'src') diff --git a/src/main.rs b/src/main.rs index c429e8a..29716e8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,18 @@ mod error; mod fs; +mod markup; mod settings; mod templated; use std::{os::unix::fs::MetadataExt, str::FromStr}; use axum::{ - body::Body, extract::Path, http::header, response::Response, routing::get, Extension, Router, + body::Body, + extract::Path, + http::{header, StatusCode}, + response::Response, + routing::get, + Extension, Router, }; use bempline::{Document, Options}; use camino::Utf8PathBuf; @@ -15,7 +21,10 @@ use fs::Filesystem; use settings::Settings; use tokio_util::io::ReaderStream; -use crate::templated::Templated; +use crate::{ + fs::{PathResolution, Webpath}, + templated::Templated, +}; #[tokio::main] async fn main() { @@ -59,11 +68,19 @@ async fn falible_handler( ) -> Result { println!("raw = {path}"); - let path = path.parse()?; + let webpath: Webpath = path.parse()?; println!("path = {path}"); - let filepath = fs.resolve(&path)?; + let PathResolution { + filepath, + is_dirfile, + } = fs.resolve(&webpath)?; + + if !webpath.is_dir() && is_dirfile { + println!("as_dir = {}", webpath.as_dir()); + return Ok(redirect(webpath.as_dir())); + } let ext = filepath.extension().unwrap_or_default(); @@ -81,6 +98,16 @@ async fn falible_handler( } } +fn redirect>(redirection: S) -> Response { + let location = redirection.into(); + println!("redirecting to {location}"); + Response::builder() + .status(StatusCode::TEMPORARY_REDIRECT) + .header(header::LOCATION, &location) + .body(Body::new(format!("redirecting to {location}"))) + .unwrap() +} + // 20 megabytes const STREAM_AFTER: u64 = 20 * 1024 * 1024; @@ -142,6 +169,13 @@ async fn send_template( templated.frontmatter.get("title").unwrap_or(filename), ); + let style_pattern = template.get_pattern("styles").unwrap(); + for style in templated.frontmatter.get_many("style") { + let mut pat = style_pattern.clone(); + pat.set("style", style); + template.set_pattern("styles", pat); + } + template.set("main", templated.content); Ok(Response::builder() diff --git a/src/markup.rs b/src/markup.rs new file mode 100644 index 0000000..4e0d66e --- /dev/null +++ b/src/markup.rs @@ -0,0 +1,218 @@ +struct State { + active_id: Option, + paragraphs: bool, + + processed: String, + current: String, + escaped_html: bool, + last_blank: bool, +} + +impl Default for State { + fn default() -> Self { + Self { + active_id: None, + paragraphs: true, + + processed: String::new(), + current: String::new(), + escaped_html: false, + last_blank: true, + } + } +} + +impl State { + /// Get an opening paragraph tag with any attributes currently set. + fn get_open_paragraph(&mut self) -> String { + match self.active_id.take() { + None => String::from("

"), + Some(id) => format!(r#"

"#), + } + } + + pub fn process_line(&mut self, line: &str) { + // we check !paragraphs here because we need to be able to enable it again + // and the easiest way right now seems to be to try to parse every + // non-paragraph line as a command + if (self.last_blank || !self.paragraphs) && self.parse_command(line) { + // don't set last_blank here. we want to be able to chain commands + return; + } + + if !self.paragraphs || !line.is_empty() { + if !self.current.is_empty() { + self.current.push('\n'); + } + + let escaped = self.escape_line(line); + self.current.push_str(escaped); + } else { + // line is empty. + self.push_current(); + } + + self.last_blank = false; + } + + pub fn done(mut self) -> String { + self.push_current(); + self.processed + } + + fn escape_line<'a>(&mut self, line: &'a str) -> &'a str { + if let Some(strip) = line.strip_prefix('\\') { + match line.chars().next() { + Some('[') => strip, + Some('<') => { + if self.last_blank { + self.escaped_html = true; + } + + strip + } + Some('\\') => strip, + _ => line, + } + } else { + line + } + } + + /// Possibly parses a line as a command and mutates internal state. + /// # Returns + /// true if the line was a command, false otherwise. + fn parse_command(&mut self, line: &str) -> bool { + match line.strip_prefix('[') { + Some(line) => match line.strip_suffix(']') { + Some(cmd) => self.run_command(cmd), + None => false, + }, + None => false, + } + } + + fn run_command(&mut self, cmd: &str) -> bool { + match cmd.trim() { + "@paragraphs off" => { + self.push_current(); + self.paragraphs = false; + true + } + "@paragraphs on" => { + self.push_current(); + self.paragraphs = true; + true + } + annotation if cmd.starts_with('#') => { + self.active_id = Some(annotation[1..].to_owned()); + true + } + _ => false, + } + } + + fn push_current(&mut self) { + if !self.current.is_empty() { + // linebreak if there is already text pushed to final + if !self.processed.is_empty() { + self.processed.push('\n'); + } + + // wrap paragraphs if all of these are true: + // - we're supposed to be wrapping paragraphs + // - either of these is true: + // - the line does not start with < + // OR + // - the line starts with < AND it's been escaped + let should_paragraph = self.paragraphs + && (!self.current.starts_with('<') + || (self.current.starts_with('<') && self.escaped_html)); + + if should_paragraph { + let open = self.get_open_paragraph(); + self.processed + .push_str(&format!("{open}\n{}\n

", self.current)); + } else { + self.processed.push_str(&self.current); + } + + // reset block dependant state + self.current.clear(); + self.last_blank = true; + self.escaped_html = false; + } + } +} + +pub fn process(raw: &str) -> String { + let mut state = State::default(); + + for line in raw.lines() { + state.process_line(line) + } + + state.done() +} + +#[cfg(test)] +mod test { + use camino::Utf8PathBuf; + + use crate::markup::process; + + #[test] + fn parses_no_commands() { + let blk1 = "line one\nline two"; + let blk2 = "block two"; + + assert_eq!(process(blk1), format!("

\n{blk1}\n

")); + + let tst = format!("{blk1}\n\n{blk2}"); + assert_eq!( + process(&tst), + format!("

\n{blk1}\n

\n

\n{blk2}\n

") + ) + } + + #[test] + fn parses_paragraph_off() { + let str = "[@paragraphs off]\none two\n\nthree\nfour"; + assert_eq!(process(str), "one two\n\nthree\nfour") + } + + #[test] + fn parses_adds_annotation() { + let str = "[#greeting]\nHello!"; + assert_eq!(process(str), "

\nHello!\n

") + } + + #[test] + fn doesnt_wrap_html() { + let str = "hello!\n\nhi, how are you?"; + assert_eq!(process(str), "

\nhello!\n

\nhi, how are you?") + } + + #[test] + fn correctly_escapes() { + let str = "\\[@paragraph on]\n\\Hello!\n\\\\Goodbye!"; + let correct = "

\n[@paragraph on]\nHello!\n\\Goodbye!\n

"; + assert_eq!(process(str), correct) + } + + const BASE: &str = "test/markup"; + fn test_files(test: &str) { + let input_path = format!("{BASE}/{test}/input.html"); + let output_path = format!("{BASE}/{test}/output.html"); + + let input = std::fs::read_to_string(input_path).unwrap(); + let output = std::fs::read_to_string(output_path).unwrap(); + + assert_eq!(process(&input), output) + } + + #[test] + fn parses_onoff() { + test_files("paragraph toggle") + } +} -- cgit 1.4.1-3-g733a5