diff options
| author | gennyble <gen@nyble.dev> | 2026-07-15 23:37:36 -0500 |
|---|---|---|
| committer | gennyble <gen@nyble.dev> | 2026-07-15 23:37:36 -0500 |
| commit | 717a183a685370bc28cb472cbc14beeb3440bed1 (patch) | |
| tree | 615dc45ebe339b448544f7e030121519f6bd035e | |
| parent | d58c67487e61684bb567583468ec4056150396b3 (diff) | |
| download | awake-717a183a685370bc28cb472cbc14beeb3440bed1.tar.gz awake-717a183a685370bc28cb472cbc14beeb3440bed1.zip | |
add [@fill KEY] directive to markup
| -rwxr-xr-x | src/main.rs | 19 | ||||
| -rwxr-xr-x | src/markup.rs | 289 | ||||
| -rw-r--r-- | test/markup/complicated fill/input.html | 16 | ||||
| -rw-r--r-- | test/markup/complicated fill/output.html | 10 | ||||
| -rw-r--r-- | test/markup/fill paragraphs/input.html | 13 | ||||
| -rw-r--r-- | test/markup/fill paragraphs/output.html | 12 |
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 |
