use crate::legacy::{Fill, Markup, intra}; pub(crate) struct State { active_id: Option, active_fill: Option, paragraphs: bool, markup: bool, processed: String, current: String, fills: Vec, escaped_html: bool, last_blank: bool, } impl Default for State { fn default() -> Self { Self { active_id: None, active_fill: None, paragraphs: true, markup: true, processed: String::new(), current: String::new(), fills: vec![], 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#"

"#), } } 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

", self.markup(content)) } else { self.markup(content) } } fn markup(&self, raw: String) -> String { if self.markup { intra::process(raw) } else { raw } } 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) { if self.parse_command(line) { // Don't set last_blank; a command does not break up a text block return; } if !line.is_empty() { self.push_current(line); self.last_blank = false; } else { 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) -> 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 { if let Some(strip) = line.strip_prefix('\\') { match strip.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.finish_current(); self.paragraphs = false; true } "@paragraphs on" => { self.finish_current(); self.paragraphs = true; true } "@markup on" => { self.finish_current(); self.markup = true; true } "@markup off" => { self.finish_current(); self.markup = false; 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 } _ => false, } } fn take_current(&mut self) -> String { self.current.split_off(0) } fn push_current(&mut self, text: &str) { if !self.current.is_empty() { self.current.push('\n'); } 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.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; } } } struct FillComand { /// Storage for the previous paragraphs variable, as @fill unsets it paragraphs: bool, key: String, content: String, } #[cfg(test)] mod test { use crate::legacy::{Fill, 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) } #[test] fn wraps_escaped_html() { let str = "\\test"; let correct = "

\ntest\n

"; assert_eq!(*process(str), correct) } #[test] fn parses_extracts_fill() { let str = "[@fill key]\nfilled stuff!\n[@fill-end]\nmeow"; let correct = "

\nmeow\n

"; 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"; 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") } }