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