diff options
-rw-r--r-- | readme.md | 30 | ||||
-rw-r--r-- | src/main.rs | 42 | ||||
-rw-r--r-- | src/markup.rs | 218 | ||||
-rw-r--r-- | test/markup/paragraph toggle/input.html | 16 | ||||
-rw-r--r-- | test/markup/paragraph toggle/output.html | 17 |
5 files changed, 318 insertions, 5 deletions
diff --git a/readme.md b/readme.md index b7a3651..e870276 100644 --- a/readme.md +++ b/readme.md @@ -20,6 +20,11 @@ What do you name the root file, then? You shouldn't have to match the webroot's directory name. Perhaps it should be configurable. I think for now we'll hard-code in `home.html`, though. +**TODO:** +we need to redirect directories to themselves with slashes. The browser thinks +that anything not ending in a slash is a file. It couldvery well be right, but +this causes chaos when we try to relative link to a directories resources. + ## Page-content uses templates Because writing the same outer-html for everything poses a few problems. I'll enumerate them for fun! @@ -64,4 +69,27 @@ a one or two sentence description of the page. <!-- not required but helps make better cards, etc. --> <meta property="og:description" content="gone for now. stepped out for a bit." /> -``` \ No newline at end of file +``` + +## Page content uses a weird kind of markup language +It's mostly just HTML, but I'm tired of writing `<p>` so damn much. + +A block of text, a textblock, are consecutive lines that contain text. A double +linebreak separates blocks. If a block starts with a `<` it's assumed to be raw +HTML and will not be wrapped in a paragraph. + +You can escape any commands or annotations, both of which are described below, +with a `\`. Like this: `\[`. You can also escape the slash itself, `\\`, or an +opening greater-than, `\<`. + +### commands + +**[@paragraphs off]** - stop wrapping text-blocks in paragrapha and just pass +them through as is. this is useful for HTML. + +**[@paragraphs on]** - start wrapping text-blocks in paragraphs starting at +the next line. + +### annotations + +**[#element-id]** - give the paragraph for this text block an ID of `element-id` \ No newline at end of file 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<Response, RuntimeError> { 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<S: Into<String>>(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<String>, + 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("<p>"), + Some(id) => format!(r#"<p id="{id}">"#), + } + } + + 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</p>", 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!("<p>\n{blk1}\n</p>")); + + let tst = format!("{blk1}\n\n{blk2}"); + assert_eq!( + process(&tst), + format!("<p>\n{blk1}\n</p>\n<p>\n{blk2}\n</p>") + ) + } + + #[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), "<p id=\"greeting\">\nHello!\n</p>") + } + + #[test] + fn doesnt_wrap_html() { + let str = "hello!\n\n<i>hi, how are you?</i>"; + assert_eq!(process(str), "<p>\nhello!\n</p>\n<i>hi, how are you?</i>") + } + + #[test] + fn correctly_escapes() { + let str = "\\[@paragraph on]\n\\<i>Hello!</i>\n\\\\Goodbye!"; + let correct = "<p>\n[@paragraph on]\n<i>Hello!</i>\n\\Goodbye!\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"); + 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") + } +} diff --git a/test/markup/paragraph toggle/input.html b/test/markup/paragraph toggle/input.html new file mode 100644 index 0000000..f6998fe --- /dev/null +++ b/test/markup/paragraph toggle/input.html @@ -0,0 +1,16 @@ +[@paragraphs off] +<style> + body { + width: 100%; + } + + main { + width: 35rem; + margin: 0 auto; + } +</style> + +[@paragraphs on] +Hello! + +welcome in. \ No newline at end of file diff --git a/test/markup/paragraph toggle/output.html b/test/markup/paragraph toggle/output.html new file mode 100644 index 0000000..fc4ad7a --- /dev/null +++ b/test/markup/paragraph toggle/output.html @@ -0,0 +1,17 @@ +<style> + body { + width: 100%; + } + + main { + width: 35rem; + margin: 0 auto; + } +</style> + +<p> +Hello! +</p> +<p> +welcome in. +</p> \ No newline at end of file |