about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--markup/src/intra.rs312
-rw-r--r--markup/src/lib.rs1
-rwxr-xr-xmarkup/src/state.rs6
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)
 		}
 	}