about summary refs log tree commit diff
path: root/markup/src
diff options
context:
space:
mode:
authorgennyble <gen@nyble.dev>2026-09-26 21:00:51 -0500
committergennyble <gen@nyble.dev>2026-09-26 21:00:51 -0500
commit0baa29ab3d1bdb13d987a221ac801250ac149dfe (patch)
tree83177115a86a1ca48613e8cfd651a0a295fc7998 /markup/src
parent92ac66f53f9fcbe758123bb8900f0214d5892da9 (diff)
coloring! also no more lineup multiline
Diffstat (limited to 'markup/src')
-rw-r--r--markup/src/breakdown.rs1
-rw-r--r--markup/src/lineup/mod.rs302
-rw-r--r--markup/src/lineup/token.rs18
-rw-r--r--markup/src/lineup/tree.rs19
4 files changed, 194 insertions, 146 deletions
diff --git a/markup/src/breakdown.rs b/markup/src/breakdown.rs
index edec7ee..1d5ed67 100644
--- a/markup/src/breakdown.rs
+++ b/markup/src/breakdown.rs
@@ -132,6 +132,7 @@ impl<'raw> Context<'raw> {
 		prefixed: PrefixedLine<'raw>,
 	) -> MultilineBlockResult<'raw> {
 		if self.code_block {
+			self.code_block = false;
 			MultilineBlockResult::Block(Block::code(
 				self.code_block_language.take(),
 				self.consume_lines(),
diff --git a/markup/src/lineup/mod.rs b/markup/src/lineup/mod.rs
index e0ae37d..2f7ebff 100644
--- a/markup/src/lineup/mod.rs
+++ b/markup/src/lineup/mod.rs
@@ -11,22 +11,9 @@ pub use tree::{Document, LinkKind, NodeStyle, TreeNode};
 pub fn process<'raw>(raw: &'raw str, solver: Rc<dyn InterlinkSolver>) -> Document<'raw> {
 	let mut r1 = round1(raw);
 	round2(raw, &mut r1);
-	let mut tree = round3(raw, &r1);
-
-	if tree.len() == 1 {
-		let node = tree.pop().unwrap();
-		if let TreeNode::Paragraph { children } = node {
-			return Document {
-				nodes: children,
-				solver,
-			};
-		} else {
-			tree.push(node);
-		}
-	}
 
 	Document {
-		nodes: tree,
+		nodes: round3(raw, &r1),
 		solver,
 	}
 }
@@ -49,6 +36,10 @@ fn round1<'raw>(raw: &'raw str) -> Vec<IndexedToken<'raw>> {
 		};
 
 		match ch {
+			' ' => {
+				chars.append_slice(Some(idx), &mut toks);
+				toks.push(IndexedToken::new(idx, Token::Space));
+			}
 			'\\' => {
 				chars.append_slice(Some(idx), &mut toks);
 				toks.push(IndexedToken::new(idx, Token::Escape));
@@ -61,6 +52,10 @@ fn round1<'raw>(raw: &'raw str) -> Vec<IndexedToken<'raw>> {
 				chars.append_slice(Some(idx), &mut toks);
 				toks.push(IndexedToken::new(idx, Token::Italic));
 			}
+			'%' => {
+				chars.append_slice(Some(idx), &mut toks);
+				toks.push(IndexedToken::new(idx, Token::Percent));
+			}
 			'|' => {
 				chars.append_slice(Some(idx), &mut toks);
 				toks.push(IndexedToken::new(idx, Token::Pipe));
@@ -77,10 +72,6 @@ fn round1<'raw>(raw: &'raw str) -> Vec<IndexedToken<'raw>> {
 				chars.append_slice(Some(idx), &mut toks);
 				// We are ignoring carriage returns here
 			}
-			'\n' => {
-				chars.append_slice(Some(idx), &mut toks);
-				toks.push(IndexedToken::new(idx, Token::Break));
-			}
 			_ch => {
 				chars.set_raw_if_empty();
 			}
@@ -91,8 +82,7 @@ fn round1<'raw>(raw: &'raw str) -> Vec<IndexedToken<'raw>> {
 }
 
 /// Folds two subsequent `Token::Itlalic` into `Token::Bold` and correctly maps
-/// `Token::Escape`. Additionally, combines successive `Token::Text` into
-/// single slices.
+/// `Token::Escape`.
 fn round2<'raw>(raw: &'raw str, toks: &mut Vec<IndexedToken<'raw>>) {
 	let mut idx = 0;
 	loop {
@@ -154,6 +144,12 @@ fn round2<'raw>(raw: &'raw str, toks: &mut Vec<IndexedToken<'raw>>) {
 				toks.remove(idx);
 				idx = idx.saturating_sub(1);
 				continue;
+			} else if pair[1].is_space() {
+				// Erroneous escape, replace it with a Text using it's index
+				let escape_idx = pair_idx[0];
+				let tok = Token::Text(&raw[escape_idx..escape_idx + 1]);
+				toks[idx] = IndexedToken::new(escape_idx, tok);
+				continue;
 			} else {
 				toks[idx + 1] = IndexedToken {
 					idx: pair_idx[1],
@@ -162,27 +158,6 @@ fn round2<'raw>(raw: &'raw str, toks: &mut Vec<IndexedToken<'raw>>) {
 			}
 		}
 
-		if let [Token::Text(text1), Token::Text(text2)] = pair {
-			let first_index = toks[idx].idx;
-			let length = text1.len() + text2.len();
-			toks[idx] = IndexedToken {
-				idx: first_index,
-				tok: Token::Text(&raw[first_index..first_index + length]),
-			};
-
-			// Remove the second text, as it's been combined into the first
-			toks.remove(idx + 1);
-
-			// Continue here so we use the same index and compare the modified
-			// Token::Text with the next token
-			continue;
-		}
-
-		if let [Token::Break, Token::Break] = pair {
-			toks.remove(idx + 1);
-			continue;
-		}
-
 		idx += 1;
 	}
 
@@ -203,21 +178,17 @@ fn round2<'raw>(raw: &'raw str, toks: &mut Vec<IndexedToken<'raw>>) {
 }
 
 fn round3<'raw, 'tok>(raw: &'raw str, toks: &'tok [IndexedToken<'raw>]) -> Vec<TreeNode<'raw>> {
-	let mut bigdoc = vec![];
-	for tok_slice in toks.split(|i| i.tok == Token::Break) {
-		bigdoc.push(TreeNode::Paragraph {
-			children: round3_intralevel(raw, tok_slice),
-		});
-	}
-
-	bigdoc
+	round3_intralevel(raw, toks)
 }
 
+const TEXT_STYLE_LIMIT: &[Token<'static>] = &[Token::LinkOpen, Token::InterlinkOpen];
+
 fn round3_intralevel<'raw, 'tok>(
 	raw: &'raw str,
 	toks: &'tok [IndexedToken<'raw>],
 ) -> Vec<TreeNode<'raw>> {
 	let mut doc = vec![];
+	let text_or_space = |it: &IndexedToken<'raw>| it.tok.is_text() || it.tok.is_space();
 
 	let mut curr = &toks[..];
 	loop {
@@ -234,13 +205,46 @@ fn round3_intralevel<'raw, 'tok>(
 			}};
 		}
 
-		if let Token::Text(_) = tok.tok {
-			decay!()
-		}
-
 		match tok.tok {
-			Token::Text(_) | Token::Pipe | Token::LinkClose | Token::InterlinkClose => decay!(),
-			Token::Break | Token::Escape => unreachable!(),
+			Token::Percent | Token::LinkClose | Token::InterlinkClose => decay!(),
+			Token::Text(_) | Token::Space => {
+				let FindCloseRet { group, remaining } = consume_while(&curr[1..], text_or_space);
+				match group.last() {
+					None => decay!(),
+					Some(last) => {
+						let end_idx = last.idx + last.tok.len();
+						doc.push(TreeNode::Text(&raw[tok.idx..end_idx]));
+						curr = remaining;
+						continue;
+					}
+				}
+			}
+			Token::Escape => unreachable!(),
+			Token::Pipe => match find_or_until(&curr[1..], Token::Pipe, TEXT_STYLE_LIMIT) {
+				None => decay!(),
+				Some(FindCloseRet { group, remaining }) => {
+					let inner = round3_intralevel(raw, group);
+					if let Some(pair) = remaining.get(..2) {
+						let (_idx, pair) = split_token_pair([pair[0], pair[1]]);
+
+						if let [Token::Percent, Token::Text(txt)] = pair {
+							curr = &remaining[2..];
+							doc.push(TreeNode::ColoredText {
+								color: txt,
+								children: inner,
+							});
+							continue;
+						}
+					}
+
+					curr = remaining;
+					doc.push(TreeNode::ColoredText {
+						color: "",
+						children: inner,
+					});
+					continue;
+				}
+			},
 			Token::InterlinkOpen => match find_close(&curr[1..], Token::InterlinkClose) {
 				None => decay!(),
 				Some(FindCloseRet { group, remaining }) => {
@@ -323,6 +327,50 @@ fn find_close<'raw, 'tok>(
 	}
 }
 
+fn find_or_until<'raw, 'tok>(
+	toks: &'tok [IndexedToken<'raw>],
+	looking: Token<'raw>,
+	limit: &[Token<'raw>],
+) -> Option<FindCloseRet<'raw, 'tok>> {
+	match toks
+		.iter()
+		.position(|tok| limit.contains(&tok.tok) || tok.tok == looking)
+	{
+		None => None,
+		Some(idx) => {
+			if toks[idx].tok == looking {
+				let group = &toks[..idx];
+				let remaining = &toks[idx + 1..];
+
+				Some(FindCloseRet { group, remaining })
+			} else {
+				None
+			}
+		}
+	}
+}
+
+fn consume_while<'raw, 'tok, F>(
+	toks: &'tok [IndexedToken<'raw>],
+	valid: F,
+) -> FindCloseRet<'raw, 'tok>
+where
+	F: Fn(&IndexedToken<'raw>) -> bool,
+{
+	match toks.iter().position(|it| !valid(it)) {
+		None => FindCloseRet {
+			group: toks,
+			remaining: &[],
+		},
+		Some(idx) => {
+			let group = &toks[..idx];
+			let remaining = &toks[idx..];
+
+			FindCloseRet { group, remaining }
+		}
+	}
+}
+
 struct FindCloseRet<'raw, 'tok> {
 	group: &'tok [IndexedToken<'raw>],
 	remaining: &'tok [IndexedToken<'raw>],
@@ -405,12 +453,19 @@ mod test {
 		round1, round2, round3, tree::LinkKind,
 	};
 
+	fn only_tokens<'raw>(it: Vec<IndexedToken<'raw>>) -> Vec<Token<'raw>> {
+		it.into_iter().map(|it| it.tok).collect()
+	}
+
 	#[test]
 	fn round1_textonly() {
 		let raw = "Raw Text!";
 		let r1 = round1(raw);
 
-		assert_eq!(r1[0].tok, Token::Text("Raw Text!"))
+		assert_eq!(
+			only_tokens(r1),
+			&[Token::Text("Raw"), Token::Space, Token::Text("Text!")]
+		)
 	}
 
 	#[test]
@@ -429,19 +484,6 @@ mod test {
 	}
 
 	#[test]
-	fn round2_collapse_texts() {
-		let raw = "RawText!";
-		let mut r1 = vec![
-			IndexedToken::new(0, Token::Text(&raw[..3])),
-			IndexedToken::new(3, Token::Text(&raw[3..])),
-		];
-
-		round2(raw, &mut r1);
-
-		assert_eq!(r1, vec![IndexedToken::new(0, Token::Text(raw))]);
-	}
-
-	#[test]
 	fn round2_finds_bold() {
 		let raw = "**";
 		let mut r1 = vec![
@@ -461,7 +503,7 @@ mod test {
 		let mut r1 = vec![
 			IndexedToken::new(0, Token::Escape),
 			IndexedToken::new(1, Token::Italic),
-			IndexedToken::new(2, Token::Text(&raw[2..3])),
+			IndexedToken::new(2, Token::Space),
 			IndexedToken::new(3, Token::Escape),
 			IndexedToken::new(4, Token::Italic),
 			IndexedToken::new(5, Token::Italic),
@@ -472,7 +514,8 @@ mod test {
 		assert_eq!(
 			r1,
 			vec![
-				IndexedToken::new(1, Token::Text("* ")),
+				IndexedToken::new(1, Token::Text("*")),
+				IndexedToken::new(2, Token::Space),
 				IndexedToken::new(4, Token::Text("**"))
 			]
 		)
@@ -484,7 +527,10 @@ mod test {
 		let mut r1 = round1(raw);
 		round2(raw, &mut r1);
 
-		assert_eq!(r1[0].tok, Token::Text("\\wrong escape"))
+		assert_eq!(
+			only_tokens(r1),
+			&[Token::Text("\\wrong"), Token::Space, Token::Text("escape")]
+		)
 	}
 
 	#[test]
@@ -494,8 +540,8 @@ mod test {
 		round2(raw, &mut r1);
 
 		assert_eq!(
-			r1,
-			vec![IndexedToken::new(0, Token::Text("wrong \\escape"))]
+			only_tokens(r1),
+			&[Token::Text("wrong"), Token::Space, Token::Text("\\escape")]
 		)
 	}
 
@@ -510,24 +556,21 @@ mod test {
 		assert_eq!(
 			r1,
 			vec![
-				IndexedToken::new(0, Token::Text("Start ")),
+				IndexedToken::new(0, Token::Text("Start")),
+				IndexedToken::new(5, Token::Space),
 				IndexedToken::new(6, Token::Bold),
 				IndexedToken::new(8, Token::Text("bold")),
 				IndexedToken::new(12, Token::Bold),
-				IndexedToken::new(14, Token::Text(" and ")),
-				IndexedToken::new(20, Token::Text("* escape"))
+				IndexedToken::new(14, Token::Space),
+				IndexedToken::new(15, Token::Text("and")),
+				IndexedToken::new(18, Token::Space),
+				IndexedToken::new(20, Token::Text("*")),
+				IndexedToken::new(21, Token::Space),
+				IndexedToken::new(22, Token::Text("escape"))
 			]
 		)
 	}
 
-	macro_rules! wrap_p {
-		($some:expr) => {
-			TreeNode::Paragraph {
-				children: vec![$some],
-			}
-		};
-	}
-
 	#[test]
 	fn round3_textonly() {
 		let raw = "Raw text!";
@@ -536,7 +579,7 @@ mod test {
 
 		let tree = round3(raw, &r1);
 
-		assert_eq!(tree, vec![wrap_p!(TreeNode::Text(raw))]);
+		assert_eq!(tree, vec![TreeNode::Text(raw)]);
 	}
 
 	#[test]
@@ -549,10 +592,10 @@ mod test {
 
 		assert_eq!(
 			tree,
-			vec![wrap_p!(TreeNode::StyledText {
+			vec![TreeNode::StyledText {
 				style: NodeStyle::Italic,
 				children: vec![TreeNode::Text("italisized!")]
-			})]
+			}]
 		)
 	}
 
@@ -566,7 +609,7 @@ mod test {
 
 		assert_eq!(
 			tree,
-			vec![wrap_p!(TreeNode::StyledText {
+			vec![TreeNode::StyledText {
 				style: NodeStyle::Italic,
 				children: vec![
 					TreeNode::Text("italics "),
@@ -576,7 +619,7 @@ mod test {
 					},
 					TreeNode::Text(" after")
 				]
-			})]
+			}]
 		)
 	}
 
@@ -589,9 +632,9 @@ mod test {
 
 		assert_eq!(
 			tree,
-			vec![wrap_p!(TreeNode::Interlink {
+			vec![TreeNode::Interlink {
 				children: vec![TreeNode::Text("link")]
-			})]
+			}]
 		)
 	}
 
@@ -604,32 +647,40 @@ mod test {
 
 		assert_eq!(
 			tree,
-			vec![wrap_p!(TreeNode::Link {
+			vec![TreeNode::Link {
 				kind: LinkKind::External,
 				name: Some(vec![TreeNode::Text("named")]),
 				children: vec![TreeNode::Text("link")]
-			})]
+			}]
 		)
 	}
 
 	#[test]
-	fn treenode_html_text() {
-		let node = TreeNode::Text("Raw Text!");
-		let rcsolve = Rc::new(EchoInterlinkSolver);
-		let actual = node.html(rcsolve);
-		let expected = "Raw Text!";
+	fn round3_colored_text() {
+		let raw = "some |text|%red that's red";
+		let mut r1 = round1(raw);
+		round2(raw, &mut r1);
+		let tree = round3(raw, &r1);
 
-		assert_eq!(actual, expected)
+		assert_eq!(
+			tree,
+			&[
+				TreeNode::Text("some "),
+				TreeNode::ColoredText {
+					color: "red",
+					children: vec![TreeNode::Text("text")]
+				},
+				TreeNode::Text(" that's red")
+			]
+		)
 	}
 
 	#[test]
-	fn treenode_html_paragraph() {
-		let node = TreeNode::Paragraph {
-			children: vec![TreeNode::Text("Para 1"), TreeNode::Text(" Para 2")],
-		};
+	fn treenode_html_text() {
+		let node = TreeNode::Text("Raw Text!");
 		let rcsolve = Rc::new(EchoInterlinkSolver);
 		let actual = node.html(rcsolve);
-		let expected = "<p>Para 1 Para 2</p>";
+		let expected = "Raw Text!";
 
 		assert_eq!(actual, expected)
 	}
@@ -698,6 +749,19 @@ mod test {
 	}
 
 	#[test]
+	fn treenode_html_colored_text() {
+		let node = TreeNode::ColoredText {
+			color: "red",
+			children: vec![TreeNode::Text("red text")],
+		};
+
+		let actual = node.html(Rc::new(Solver));
+		let expected = "<span style='color: red'>red text</span>";
+
+		assert_eq!(actual, expected)
+	}
+
+	#[test]
 	fn document_html_complex() {
 		let raw = "*italics **bold** after*";
 		let expected = "<i>italics <b>bold</b> after</i>";
@@ -707,32 +771,4 @@ mod test {
 
 		assert_eq!(html, expected)
 	}
-
-	#[test]
-	fn multiparagraph() {
-		let raw = "One\n\nTwo";
-
-		let expected = vec![
-			wrap_p!(TreeNode::Text("One")),
-			wrap_p!(TreeNode::Text("Two")),
-		];
-
-		let doc = process(raw, Rc::new(EchoInterlinkSolver));
-
-		assert_eq!(doc.nodes, expected)
-	}
-
-	#[test]
-	fn multiparagraph_carriagereturn() {
-		let raw = "One\r\n\r\nTwo";
-
-		let expected = vec![
-			wrap_p!(TreeNode::Text("One")),
-			wrap_p!(TreeNode::Text("Two")),
-		];
-
-		let doc = process(raw, Rc::new(EchoInterlinkSolver));
-
-		assert_eq!(doc.nodes, expected)
-	}
 }
diff --git a/markup/src/lineup/token.rs b/markup/src/lineup/token.rs
index 5b61597..78d2311 100644
--- a/markup/src/lineup/token.rs
+++ b/markup/src/lineup/token.rs
@@ -14,10 +14,10 @@ impl<'raw> IndexedToken<'raw> {
 
 #[derive(Copy, Clone, Debug, PartialEq)]
 pub enum Token<'raw> {
-	/// Raw, no markup
+	/// String of non-space characters
 	Text(&'raw str),
-	/// Linebreak
-	Break,
+	/// A space; ' '; 0x20
+	Space,
 	/// backtick
 	Code,
 	/// asterisk
@@ -26,6 +26,8 @@ pub enum Token<'raw> {
 	Bold,
 	/// backslash
 	Escape,
+	/// Percent %
+	Percent,
 	/// Pipe character |
 	Pipe,
 	/// Link Opening [
@@ -47,13 +49,18 @@ impl<'raw> Token<'raw> {
 		}
 	}
 
+	pub fn is_space(&self) -> bool {
+		if let Self::Space = self { true } else { false }
+	}
+
 	pub fn len(&self) -> usize {
 		match self {
 			Self::Text(txt) => txt.len(),
-			Self::Break
+			Self::Space
 			| Self::Code
 			| Self::Italic
 			| Self::Escape
+			| Self::Percent
 			| Self::Pipe
 			| Self::LinkOpen
 			| Self::LinkClose => 1,
@@ -64,9 +71,10 @@ impl<'raw> Token<'raw> {
 	pub fn as_str(&self) -> &'raw str {
 		match self {
 			Token::Bold => "**",
-			Token::Break => "\n",
+			Token::Space => " ",
 			Token::Code => "`",
 			Token::Escape => "\\",
+			Token::Percent => "*",
 			Token::Pipe => "|",
 			Token::LinkClose => "]",
 			Token::LinkOpen => "[",
diff --git a/markup/src/lineup/tree.rs b/markup/src/lineup/tree.rs
index bf81545..68a9493 100644
--- a/markup/src/lineup/tree.rs
+++ b/markup/src/lineup/tree.rs
@@ -20,9 +20,6 @@ impl<'raw> Document<'raw> {
 #[derive(Clone, Debug, PartialEq)]
 pub enum TreeNode<'raw> {
 	Text(&'raw str),
-	Paragraph {
-		children: Vec<TreeNode<'raw>>,
-	},
 	Interlink {
 		children: Vec<TreeNode<'raw>>,
 	},
@@ -35,6 +32,10 @@ pub enum TreeNode<'raw> {
 		style: NodeStyle,
 		children: Vec<TreeNode<'raw>>,
 	},
+	ColoredText {
+		color: &'raw str,
+		children: Vec<TreeNode<'raw>>,
+	},
 }
 
 #[derive(Clone, Copy, Debug, PartialEq)]
@@ -53,10 +54,6 @@ impl<'raw> TreeNode<'raw> {
 	pub fn html(&self, solver: Rc<dyn InterlinkSolver>) -> Cow<'raw, str> {
 		match self {
 			TreeNode::Text(str) => Cow::Borrowed(str),
-			TreeNode::Paragraph { children } => {
-				let inner = Self::html_nodes(children.as_slice(), solver);
-				Cow::Owned(format!("<p>{inner}</p>"))
-			}
 			TreeNode::Interlink { children } => {
 				let txt = Self::text_nodes(&children, Rc::clone(&solver));
 
@@ -93,13 +90,16 @@ impl<'raw> TreeNode<'raw> {
 				let inner = Self::html_nodes(children.as_slice(), solver);
 				Cow::Owned(format!("{open}{inner}{close}"))
 			}
+			TreeNode::ColoredText { color, children } => {
+				let inner = Self::html_nodes(children.as_slice(), solver);
+				Cow::Owned(format!("<span style='color: {color}'>{inner}</span>"))
+			}
 		}
 	}
 
 	pub fn text(&self, solver: Rc<dyn InterlinkSolver>) -> Cow<'raw, str> {
 		match self {
 			TreeNode::Text(str) => Cow::Borrowed(str),
-			TreeNode::Paragraph { children } => Cow::Owned(Self::text_nodes(children, solver)),
 			TreeNode::Interlink { children } => {
 				let text = Self::text_nodes(children, Rc::clone(&solver));
 				Cow::Owned(solver.link_title(&text).into_owned())
@@ -121,6 +121,9 @@ impl<'raw> TreeNode<'raw> {
 			TreeNode::StyledText { style: _, children } => {
 				Cow::Owned(Self::text_nodes(children, solver))
 			}
+			TreeNode::ColoredText { color: _, children } => {
+				Cow::Owned(Self::text_nodes(children, solver))
+			}
 		}
 	}