diff options
| -rw-r--r-- | markup/src/intra.rs | 312 | ||||
| -rw-r--r-- | markup/src/lib.rs | 1 | ||||
| -rwxr-xr-x | markup/src/state.rs | 6 |
3 files changed, 316 insertions, 3 deletions
diff --git a/markup/src/intra.rs b/markup/src/intra.rs new file mode 100644 index 0000000..e936e39 --- /dev/null +++ b/markup/src/intra.rs @@ -0,0 +1,312 @@ +use core::range; +use std::fmt::Display; +use std::ops::RangeInclusive; + +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 { + 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<ContextualizedString>, +} + +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<ContextualizedString> { + 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 => "<code>", + 2 => "<i>", + 4 => "<b>", + + // Double + 3 => "<i><code>", + 5 => "<b><code>", + 6 => "<b><i>", + + // Triple + 7 => "<b><i><code>", + + mask_match_unreachable!() => unreachable!(), + } + } + + pub fn close(&self) -> &'static str { + match self.mask() { + // Nothing + 0 => "", + + // Single + 1 => "</code>", + 2 => "</i>", + 4 => "</b>", + + // Double + 3 => "</code></i>", + 5 => "<code></b>", + 6 => "</i></b>", + + // Triple + 7 => "</code></i></b>", + + 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 crate::intra::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{}`" -> "<code>struct foo{}</code>"); + verify!(r"`let code = '\`';`" -> "<code>let code = '`';</code>"); + } + + #[test] + fn parses_italic() { + verify!("*(aside)*" -> "<i>(aside)</i>"); + verify!(r"*(italics are \*)*" -> "<i>(italics are *)</i>"); + verify!("*italics**again*" -> "<i>italics</i><i>again</i>"); + } + + #[test] + fn parses_bold() { + verify!("**bold!**" -> "<b>bold!</b>"); + verify!("*italics***bold!**" -> "<i>italics</i><b>bold!</b>"); + } + + #[test] + fn parses_code_italics() { + verify!("*`italic code`*" -> "<i><code>italic code</code></i>"); + verify!("`*italic code*`" -> "<i><code>italic code</code></i>"); + + verify!("`program *italics* more`" + -> "<code>program </code>\ + <i><code>italics</code></i>\ + <code> more</code>"); + } +} diff --git a/markup/src/lib.rs b/markup/src/lib.rs index 1594d5b..c4d7918 100644 --- a/markup/src/lib.rs +++ b/markup/src/lib.rs @@ -1,3 +1,4 @@ +mod intra; mod state; use std::ops::Deref; diff --git a/markup/src/state.rs b/markup/src/state.rs index 6d47f67..5931376 100755 --- a/markup/src/state.rs +++ b/markup/src/state.rs @@ -1,4 +1,4 @@ -use crate::{Fill, Markup}; +use crate::{Fill, Markup, intra}; pub(crate) struct State { active_id: Option<String>, @@ -61,9 +61,9 @@ impl State { 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) + format!("{open}\n{}\n</p>", intra::process(content)) } else { - content + intra::process(content) } } |
