about summary refs log tree commit diff
diff options
context:
space:
mode:
authorgennyble <gen@nyble.dev>2026-07-15 23:37:36 -0500
committergennyble <gen@nyble.dev>2026-07-15 23:37:36 -0500
commit717a183a685370bc28cb472cbc14beeb3440bed1 (patch)
tree615dc45ebe339b448544f7e030121519f6bd035e
parentd58c67487e61684bb567583468ec4056150396b3 (diff)
downloadawake-717a183a685370bc28cb472cbc14beeb3440bed1.tar.gz
awake-717a183a685370bc28cb472cbc14beeb3440bed1.zip
add [@fill KEY] directive to markup
-rwxr-xr-xsrc/main.rs19
-rwxr-xr-xsrc/markup.rs289
-rw-r--r--test/markup/complicated fill/input.html16
-rw-r--r--test/markup/complicated fill/output.html10
-rw-r--r--test/markup/fill paragraphs/input.html13
-rw-r--r--test/markup/fill paragraphs/output.html12
6 files changed, 300 insertions, 59 deletions
diff --git a/src/main.rs b/src/main.rs
index e381e55..3501e1f 100755
--- a/src/main.rs
+++ b/src/main.rs
@@ -46,6 +46,7 @@ use util::{Referer, RemoteIp, SessionId};
 
 use crate::{
 	fs::{PathResolution, Webpath},
+	markup::Markup,
 	templated::Templated,
 };
 
@@ -416,22 +417,26 @@ async fn send_template(
 	tracing::trace!("finished published block");
 
 	// insert the page content itself
-	let mut markedup = markup::process(&templated.content);
+	let markedup = markup::process(&templated.content);
+	markedup.fill_document(&mut template);
+
 	if templated.frontmatter.get("use-template").is_some() {
-		markedup = template_content(state, &templated.frontmatter, markedup);
+		let main = template_content(state, &templated.frontmatter, markedup);
+		template.set("main", main);
+	} else {
+		template.set("main", markedup.into_content());
 	}
 
-	template.set("main", markedup);
-
 	Ok(Response::builder()
 		.header(header::CONTENT_TYPE, "text/html")
 		.body(Body::from(template.compile()))
 		.unwrap())
 }
 
-fn template_content(state: AwakeState, frontmatter: &Frontmatter, marked: String) -> String {
-	let Ok(mut doc) = Document::from_str(&marked, Options::default()) else {
-		return marked;
+fn template_content(state: AwakeState, frontmatter: &Frontmatter, marked: Markup) -> String {
+	let mut doc = match marked.into_document() {
+		Ok(doc) => doc,
+		Err(content) => return content,
 	};
 
 	if frontmatter.get("system-stats").is_some() {
diff --git a/src/markup.rs b/src/markup.rs
index 289bc1f..bde28ee 100755
--- a/src/markup.rs
+++ b/src/markup.rs
@@ -1,9 +1,15 @@
+use std::{borrow::Cow, ops::Deref};
+
+use bempline::{Document, Options};
+
 struct State {
 	active_id: Option<String>,
+	active_fill: Option<FillComand>,
 	paragraphs: bool,
 
 	processed: String,
 	current: String,
+	fills: Vec<Fill>,
 	escaped_html: bool,
 	last_blank: bool,
 }
@@ -12,10 +18,12 @@ impl Default for State {
 	fn default() -> Self {
 		Self {
 			active_id: None,
+			active_fill: None,
 			paragraphs: true,
 
 			processed: String::new(),
 			current: String::new(),
+			fills: vec![],
 			escaped_html: false,
 			last_blank: true,
 		}
@@ -31,33 +39,79 @@ impl State {
 		}
 	}
 
+	fn paragraph(&mut self) {
+		let current = self.take_current();
+		let content = self.make_paragraph(current);
+
+		if let Some(fill) = self.active_fill.as_mut() {
+			// linebreak if there is already text pushed to fill.content
+			if !fill.content.is_empty() {
+				fill.content.push('\n');
+			}
+
+			fill.content.push_str(&content);
+		} else {
+			// linebreak if there is already text pushed to final
+			if !self.processed.is_empty() {
+				self.processed.push('\n');
+			}
+
+			self.processed.push_str(&content);
+		}
+	}
+
+	fn make_paragraph(&mut self, content: String) -> String {
+		if self.should_paragraph_string(&content) {
+			let open = self.get_open_paragraph();
+			format!("{open}\n{}\n</p>", content)
+		} else {
+			content
+		}
+	}
+
+	fn should_paragraph_string(&self, string: &str) -> bool {
+		// 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
+		self.paragraphs
+			&& (!string.starts_with('<') || (string.starts_with('<') && self.escaped_html))
+	}
+
 	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
+		if self.parse_command(line) {
+			// Don't set last_blank; a command does not break up a text block
 			return;
 		}
 
-		if !self.paragraphs || !line.is_empty() {
-			if !self.current.is_empty() {
-				self.current.push('\n');
-			}
+		if !line.is_empty() {
+			self.push_current(line);
 
-			let escaped = self.escape_line(line);
-			self.current.push_str(escaped);
-			
 			self.last_blank = false;
 		} else {
-			// line is empty.
-			self.push_current();
+			if !self.paragraphs {
+				// It's a blank line, but we're not doing paragraphs,
+				// so leave it in the document
+				self.current.push('\n');
+				self.last_blank = false;
+				return;
+			}
+
+			// line is empty, close the textblock
+			self.finish_current();
 		}
 	}
 
-	pub fn done(mut self) -> String {
-		self.push_current();
-		self.processed
+	pub fn done(mut self) -> Markup {
+		self.finish_current();
+		self.finish_active_fill();
+
+		Markup {
+			content: self.processed,
+			fill: self.fills,
+		}
 	}
 
 	fn escape_line<'a>(&mut self, line: &'a str) -> &'a str {
@@ -95,15 +149,33 @@ impl State {
 	fn run_command(&mut self, cmd: &str) -> bool {
 		match cmd.trim() {
 			"@paragraphs off" => {
-				self.push_current();
+				self.finish_current();
 				self.paragraphs = false;
 				true
 			}
 			"@paragraphs on" => {
-				self.push_current();
+				self.finish_current();
 				self.paragraphs = true;
 				true
 			}
+			"@fill-end" => {
+				self.finish_current();
+				self.finish_active_fill();
+				true
+			}
+			template_key if cmd.starts_with("@fill ") => {
+				self.finish_current();
+
+				let key = template_key.strip_prefix("@fill ").unwrap().to_owned();
+				self.active_fill = Some(FillComand {
+					paragraphs: self.paragraphs,
+					key,
+					content: String::new(),
+				});
+				self.paragraphs = false;
+
+				true
+			}
 			annotation if cmd.starts_with('#') => {
 				self.active_id = Some(annotation[1..].to_owned());
 				true
@@ -112,64 +184,125 @@ impl State {
 		}
 	}
 
-	fn push_current(&mut self) {
+	fn take_current(&mut self) -> String {
+		self.current.split_off(0)
+	}
+
+	fn push_current(&mut self, text: &str) {
 		if !self.current.is_empty() {
-			// linebreak if there is already text pushed to final
-			if !self.processed.is_empty() {
-				self.processed.push('\n');
-			}
+			self.current.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);
-			}
+		let escaped = self.escape_line(text);
+		self.current.push_str(escaped);
+	}
+
+	fn finish_current(&mut self) {
+		if !self.current.is_empty() {
+			// Calling paragraph clears self.current
+			self.paragraph();
 
 			// reset block dependant state
-			self.current.clear();
 			self.last_blank = true;
 			self.escaped_html = false;
 		}
 	}
+
+	fn finish_active_fill(&mut self) {
+		if let Some(fill) = self.active_fill.take() {
+			self.fills.push(Fill {
+				key: fill.key,
+				content: fill.content,
+			});
+
+			self.paragraphs = fill.paragraphs;
+		}
+	}
+}
+
+pub struct Markup {
+	content: String,
+	fill: Vec<Fill>,
 }
 
-pub fn process(raw: &str) -> String {
+impl Markup {
+	pub fn fill(&self) -> &[Fill] {
+		&self.fill
+	}
+
+	pub fn content(&self) -> &str {
+		&self.content
+	}
+
+	pub fn into_content(self) -> String {
+		self.content
+	}
+
+	pub fn into_document(self) -> Result<Document, String> {
+		let Ok(mut doc) = Document::from_str(&self.content, Options::default()) else {
+			return Err(self.content);
+		};
+
+		self.fill_document(&mut doc);
+
+		Ok(doc)
+	}
+
+	pub fn fill_document(&self, doc: &mut Document) {
+		for fill in &self.fill {
+			doc.set(&fill.key, &fill.content);
+		}
+	}
+}
+
+impl Deref for Markup {
+	type Target = String;
+
+	fn deref(&self) -> &Self::Target {
+		&self.content
+	}
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct Fill {
+	pub key: String,
+	pub content: String,
+}
+
+pub fn process(raw: &str) -> Markup {
 	tracing::trace!("processing!");
 	let mut state = State::default();
 
 	for line in raw.lines() {
+		println!("raw line: '{line}'");
 		state.process_line(line)
 	}
 
 	state.done()
 }
 
+struct FillComand {
+	/// Storage for the previous paragraphs variable, as @fill unsets it
+	paragraphs: bool,
+
+	key: String,
+	content: String,
+}
+
 #[cfg(test)]
 mod test {
-	use crate::markup::process;
+	use crate::markup::{process, Fill};
 
 	#[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>"));
+		assert_eq!(*process(blk1), format!("<p>\n{blk1}\n</p>"));
 
 		let tst = format!("{blk1}\n\n{blk2}");
 		assert_eq!(
-			process(&tst),
+			*process(&tst),
 			format!("<p>\n{blk1}\n</p>\n<p>\n{blk2}\n</p>")
 		)
 	}
@@ -177,33 +310,48 @@ mod test {
 	#[test]
 	fn parses_paragraph_off() {
 		let str = "[@paragraphs off]\none two\n\nthree\nfour";
-		assert_eq!(process(str), "one 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>")
+		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>")
+		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)
+		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)
+		assert_eq!(*process(str), correct)
+	}
+
+	#[test]
+	fn parses_extracts_fill() {
+		let str = "[@fill key]\nfilled stuff!\n[@fill-end]\nmeow";
+		let correct = "<p>\nmeow\n</p>";
+		let marked = process(str);
+		assert_eq!(
+			marked.fill,
+			vec![Fill {
+				key: "key".into(),
+				content: "filled stuff!".into()
+			}]
+		);
+		assert_eq!(marked.content, correct)
 	}
 
 	const BASE: &str = "test/markup";
@@ -214,7 +362,7 @@ mod test {
 		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)
+		assert_eq!(*process(&input), output)
 	}
 
 	#[test]
@@ -222,3 +370,40 @@ mod test {
 		test_files("paragraph toggle")
 	}
 }
+
+#[cfg(test)]
+mod document_test {
+	use crate::markup::process;
+
+	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();
+
+		let actual = process(&input).into_document().unwrap().compile();
+
+		assert_eq!(actual, output)
+	}
+
+	#[test]
+	fn inserts_fill() {
+		let input = "[@fill main]\nwords!\n[@fill-end]\n{main}";
+		let expected = "<p>\nwords!\n</p>";
+		let markup = process(input);
+		let doc = markup.into_document().unwrap().compile();
+		assert_eq!(doc, expected)
+	}
+
+	#[test]
+	fn fill_restores_paragraphs() {
+		test_files("fill paragraphs");
+	}
+
+	#[test]
+	fn parses_complicated_fill() {
+		test_files("complicated fill");
+	}
+}
diff --git a/test/markup/complicated fill/input.html b/test/markup/complicated fill/input.html
new file mode 100644
index 0000000..a412f55
--- /dev/null
+++ b/test/markup/complicated fill/input.html
@@ -0,0 +1,16 @@
+<section>
+	<h2>{title}</h2>
+	{main}
+</section>
+
+[@fill title]
+Title!
+[@fill-end]
+
+[@fill main]
+[@paragraphs on]
+These are some words in
+a paragraph!
+
+And a new paragraph!
+[@fill-end]
\ No newline at end of file
diff --git a/test/markup/complicated fill/output.html b/test/markup/complicated fill/output.html
new file mode 100644
index 0000000..7696c4c
--- /dev/null
+++ b/test/markup/complicated fill/output.html
@@ -0,0 +1,10 @@
+<section>
+	<h2>Title!</h2>
+	<p>
+These are some words in
+a paragraph!
+</p>
+<p>
+And a new paragraph!
+</p>
+</section>
\ No newline at end of file
diff --git a/test/markup/fill paragraphs/input.html b/test/markup/fill paragraphs/input.html
new file mode 100644
index 0000000..44d001a
--- /dev/null
+++ b/test/markup/fill paragraphs/input.html
@@ -0,0 +1,13 @@
+Paragraphs are on!
+
+[@paragraphs off]
+Before main! Paragraphs off!
+
+|{main}|
+[@fill main]
+Some words with no paragraphs!
+[@paragraphs on]
+Some words with paragraphs!
+[@fill-end]
+
+Paragraphs should still be off!
\ No newline at end of file
diff --git a/test/markup/fill paragraphs/output.html b/test/markup/fill paragraphs/output.html
new file mode 100644
index 0000000..c71e982
--- /dev/null
+++ b/test/markup/fill paragraphs/output.html
@@ -0,0 +1,12 @@
+<p>
+Paragraphs are on!
+</p>
+Before main! Paragraphs off!
+
+|Some words with no paragraphs!
+<p>
+Some words with paragraphs!
+</p>|
+
+
+Paragraphs should still be off!
\ No newline at end of file