about summary refs log tree commit diff
diff options
context:
space:
mode:
authorgennyble <gen@nyble.dev>2024-02-24 02:24:50 -0600
committergennyble <gen@nyble.dev>2024-02-24 02:24:50 -0600
commitd716ffb05f086061d9a5302f67f3695457fcff02 (patch)
tree1232bbdf657e50d78a960b57ce0db6f288384ca2
parent9976c697f4a562da22aad36786878073c8acd4bb (diff)
downloadawake-d716ffb05f086061d9a5302f67f3695457fcff02.tar.gz
awake-d716ffb05f086061d9a5302f67f3695457fcff02.zip
Implement basic markup system
this also has the redirection from non-trailing-slash-directories to their slashy counterparts
-rw-r--r--readme.md30
-rw-r--r--src/main.rs42
-rw-r--r--src/markup.rs218
-rw-r--r--test/markup/paragraph toggle/input.html16
-rw-r--r--test/markup/paragraph toggle/output.html17
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