use std::fmt::Display; macro_rules! false_or_continue { ($action:expr) => { if $action { continue; } }; } pub fn process(raw: String) -> String { let mut chars = raw.chars(); let mut ctx = Context::default(); loop { let nch = match chars.next() { None => break, Some(ch) => ch, }; false_or_continue!(escaped_char(&mut ctx, nch)); match nch { '`' => ctx.toggle_code(), '*' => ctx.toggle_italics(), ch => ctx.push_char(ch), } } let out = ctx.done(); let length = out.iter().fold(0, |acc, cs| acc + cs.len()); let mut string = String::with_capacity(length); out.iter().for_each(|cs| string.push_str(&cs.to_string())); string } #[inline] fn escaped_char(ctx: &mut Context, ch: char) -> bool { let escaped_chars = ['`', '*', '\\']; if ctx.escape_next { ctx.escape_next = false; if escaped_chars.contains(&ch) { ctx.push_char(ch); true } else { ctx.push_char('\\'); false } } else if ch == '\\' { ctx.escape_next = true; true } else { false } } struct ContextualizedString { state: State, text: String, } impl ContextualizedString { pub fn len(&self) -> usize { self.state.open().len() + self.text.len() + self.state.close().len() } } #[derive(Copy, Clone, Debug, PartialEq)] struct State { code: bool, italics: bool, bold: bool, } struct Context { escape_next: bool, bold_flag: bool, text_state: State, current: String, output: Vec, } impl Context { pub fn push_char(&mut self, ch: char) { if self.bold_flag { // Attemping to push with the bold flag set, // pop_current as italic, clear the italic flag, // and then push self.pop_current(); self.text_state.italics = false; } self.current.push(ch); } pub fn toggle_code(&mut self) { self.pop_current(); self.text_state.code = !self.text_state.code; } pub fn toggle_italics(&mut self) { if self.text_state.bold { if self.bold_flag { // We're bold and the flag is set! pop_current and disable bolding self.pop_current(); self.text_state.bold = false; self.bold_flag = false; return; } else { // bold and flag not set, set flag and return self.bold_flag = true; return; } } else { if self.text_state.italics && self.current.is_empty() { // we're already italisized and nothing is in current, // this is a "**", a bold self.text_state.italics = false; self.text_state.bold = true; return; } else { // Passed all bold checks, pop_current() and set the italic flag self.pop_current(); self.text_state.italics = !self.text_state.italics; } } } pub fn done(mut self) -> Vec { self.pop_current(); self.output } fn pop_current(&mut self) { if !self.current.is_empty() { let cs = ContextualizedString { state: self.text_state, text: self.take_current(), }; self.output.push(cs); } } fn take_current(&mut self) -> String { self.current.split_off(0) } } macro_rules! mask_match_unreachable { () => { State::MASK_MAX_PP..=u8::MAX }; } impl State { const MASK_MAX: u8 = 7; const MASK_MAX_PP: u8 = State::MASK_MAX + 1; pub fn mask(&self) -> u8 { // code 1, italics 2, bold 4 self.code as u8 | ((self.italics as u8) << 1) | ((self.bold as u8) << 2) } pub fn open(&self) -> &'static str { match self.mask() { // Nothing 0 => "", // Single 1 => "", 2 => "", 4 => "", // Double 3 => "", 5 => "", 6 => "", // Triple 7 => "", mask_match_unreachable!() => unreachable!(), } } pub fn close(&self) -> &'static str { match self.mask() { // Nothing 0 => "", // Single 1 => "", 2 => "", 4 => "", // Double 3 => "", 5 => "", 6 => "", // Triple 7 => "", mask_match_unreachable!() => unreachable!(), } } } impl Display for ContextualizedString { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}{}{}", self.state.open(), self.text, self.state.close() ) } } // MARK: defaults impl Default for Context { fn default() -> Self { Self { escape_next: false, bold_flag: false, text_state: State::default(), current: String::with_capacity(128), output: vec![], } } } impl Default for State { fn default() -> Self { Self { code: false, italics: false, bold: false, } } } // MARK: tests #[cfg(test)] mod test { use super::process; macro_rules! verify { ($inp:literal -> $out:literal) => {{ let actual = process($inp.to_string()); if actual != $out { panic!("expected: \"{}\"\n actual: \"{actual}\"", $out); } }}; } #[test] fn parese_no_markup() { verify!("no markup" -> "no markup"); verify!("no\nmarkup" -> "no\nmarkup") } #[test] fn parses_escapes() { verify!(r"italic \*" -> "italic *"); verify!(r"code \`" -> "code `"); verify!(r"escape \\" -> r"escape \"); verify!(r"everything \\ \* \`" -> r"everything \ * `"); } #[test] fn parses_code() { verify!("`struct foo{}`" -> "struct foo{}"); verify!(r"`let code = '\`';`" -> "let code = '`';"); } #[test] fn parses_italic() { verify!("*(aside)*" -> "(aside)"); verify!(r"*(italics are \*)*" -> "(italics are *)"); verify!("*italics**again*" -> "italicsagain"); } #[test] fn parses_bold() { verify!("**bold!**" -> "bold!"); verify!("*italics***bold!**" -> "italicsbold!"); } #[test] fn parses_code_italics() { verify!("*`italic code`*" -> "italic code"); verify!("`*italic code*`" -> "italic code"); verify!("`program *italics* more`" -> "program \ italics\ more"); } }