about summary refs log tree commit diff
path: root/markup/src
diff options
context:
space:
mode:
authorgennyble <gen@nyble.dev>2026-09-21 14:23:07 -0500
committergennyble <gen@nyble.dev>2026-09-21 14:23:07 -0500
commitbe872aa0cd61ea16b95eddcd68c6a72f0fe3af96 (patch)
tree8935da5f9639b939a0e5ecd2bba3072d71d602bc /markup/src
parent178ca3c066a1ef920b112da741625099b6dc07da (diff)
massive reorg
Diffstat (limited to 'markup/src')
-rw-r--r--markup/src/lineup/interlinksolver.rs17
-rw-r--r--markup/src/lineup/mod.rs (renamed from markup/src/lineup.rs)365
-rw-r--r--markup/src/lineup/token.rs85
-rw-r--r--markup/src/lineup/tree.rs147
4 files changed, 373 insertions, 241 deletions
diff --git a/markup/src/lineup/interlinksolver.rs b/markup/src/lineup/interlinksolver.rs
new file mode 100644
index 0000000..074fadf
--- /dev/null
+++ b/markup/src/lineup/interlinksolver.rs
@@ -0,0 +1,17 @@
+use std::borrow::Cow;
+
+pub trait InterlinkSolver {
+	fn link_title<'n>(&self, name: &'n str) -> Cow<'n, str>;
+	fn link_location<'n>(&self, name: &'n str) -> Cow<'n, str>;
+}
+
+pub struct EchoInterlinkSolver;
+impl InterlinkSolver for EchoInterlinkSolver {
+	fn link_title<'n>(&self, name: &'n str) -> Cow<'n, str> {
+		Cow::Borrowed(name)
+	}
+
+	fn link_location<'n>(&self, name: &'n str) -> Cow<'n, str> {
+		Cow::Borrowed(name)
+	}
+}
diff --git a/markup/src/lineup.rs b/markup/src/lineup/mod.rs
index fa5dbae..0e1ecce 100644
--- a/markup/src/lineup.rs
+++ b/markup/src/lineup/mod.rs
@@ -1,5 +1,12 @@
-use core::fmt;
-use std::{borrow::Cow, iter::Enumerate, rc::Rc, str::Chars};
+mod interlinksolver;
+mod token;
+mod tree;
+
+use std::{iter::Enumerate, rc::Rc, str::Chars};
+
+pub use interlinksolver::{EchoInterlinkSolver, InterlinkSolver};
+use token::{IndexedToken, Token};
+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);
@@ -54,13 +61,17 @@ 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::Pipe));
+			}
 			'[' => {
 				chars.append_slice(Some(idx), &mut toks);
-				toks.push(IndexedToken::new(idx, Token::InterlinkOpen));
+				toks.push(IndexedToken::new(idx, Token::LinkOpen));
 			}
 			']' => {
 				chars.append_slice(Some(idx), &mut toks);
-				toks.push(IndexedToken::new(idx, Token::InterlinkClose));
+				toks.push(IndexedToken::new(idx, Token::LinkClose));
 			}
 			'\r' => {
 				chars.append_slice(Some(idx), &mut toks);
@@ -102,6 +113,22 @@ fn round2<'raw>(raw: &'raw str, toks: &mut Vec<IndexedToken<'raw>>) {
 				toks.remove(idx + 1);
 				continue;
 			}
+			[Token::LinkOpen, Token::LinkOpen] => {
+				toks[idx] = IndexedToken {
+					idx: indicies[0],
+					tok: Token::InterlinkOpen,
+				};
+				toks.remove(idx + 1);
+				continue;
+			}
+			[Token::LinkClose, Token::LinkClose] => {
+				toks[idx] = IndexedToken {
+					idx: indicies[0],
+					tok: Token::InterlinkClose,
+				};
+				toks.remove(idx + 1);
+				continue;
+			}
 			_ => (),
 		}
 
@@ -186,10 +213,6 @@ fn round3<'raw, 'tok>(raw: &'raw str, toks: &'tok [IndexedToken<'raw>]) -> Vec<T
 	bigdoc
 }
 
-fn split_token_pair<'raw>(it: [IndexedToken<'raw>; 2]) -> ([usize; 2], [Token<'raw>; 2]) {
-	([it[0].idx, it[1].idx], [it[0].tok, it[1].tok])
-}
-
 fn round3_intralevel<'raw, 'tok>(
 	raw: &'raw str,
 	toks: &'tok [IndexedToken<'raw>],
@@ -203,32 +226,23 @@ fn round3_intralevel<'raw, 'tok>(
 		}
 
 		let tok = curr[0];
+		macro_rules! decay {
+			() => {{
+				doc.push(TreeNode::Text(tok.tok.as_str()));
+				curr = &curr[1..];
+				continue;
+			}};
+		}
+
 		if let Token::Text(str) = tok.tok {
-			doc.push(TreeNode::Text(str));
-			curr = &curr[1..];
-			continue;
+			decay!()
 		}
 
 		match tok.tok {
-			Token::Text(str) => {
-				doc.push(TreeNode::Text(str));
-				curr = &curr[1..];
-				continue;
-			}
-			Token::Break | Token::Escape => {
-				unreachable!()
-			}
-			Token::InterlinkClose => {
-				doc.push(TreeNode::Text("]"));
-				curr = &curr[1..];
-				continue;
-			}
+			Token::Text(_) | Token::Pipe | Token::LinkClose | Token::InterlinkClose => decay!(),
+			Token::Break | Token::Escape => unreachable!(),
 			Token::InterlinkOpen => match find_close(&curr[1..], Token::InterlinkClose) {
-				None => {
-					doc.push(TreeNode::Text(tok.tok.as_str()));
-					curr = &curr[1..];
-					continue;
-				}
+				None => decay!(),
 				Some(FindCloseRet { group, remaining }) => {
 					let inner = round3_intralevel(raw, group);
 					curr = remaining;
@@ -236,12 +250,38 @@ fn round3_intralevel<'raw, 'tok>(
 					continue;
 				}
 			},
-			Token::Bold | Token::Code | Token::Italic => match find_close(&curr[1..], tok.tok) {
-				None => {
-					doc.push(TreeNode::Text(tok.tok.as_str()));
-					curr = &curr[1..];
+			Token::LinkOpen => match find_close(&curr[1..], Token::LinkClose) {
+				None => decay!(),
+				Some(FindCloseRet { group, remaining }) => {
+					curr = remaining;
+
+					let mut splits = group.splitn(2, |tok| tok.tok == Token::Pipe);
+					let first = splits.next();
+					let second = splits.next();
+
+					match (first, second) {
+						(Some(one), None) => {
+							doc.push(TreeNode::Link {
+								kind: LinkKind::External,
+								name: None,
+								children: round3_intralevel(raw, one),
+							});
+						}
+						(Some(one), Some(two)) => {
+							doc.push(TreeNode::Link {
+								kind: LinkKind::External,
+								name: Some(round3_intralevel(raw, one)),
+								children: round3_intralevel(raw, two),
+							});
+						}
+						_ => unreachable!(),
+					}
+
 					continue;
 				}
+			},
+			Token::Bold | Token::Code | Token::Italic => match find_close(&curr[1..], tok.tok) {
+				None => decay!(),
 				Some(FindCloseRet { group, remaining }) => {
 					let inner = round3_intralevel(raw, group);
 					curr = remaining;
@@ -264,6 +304,10 @@ fn round3_intralevel<'raw, 'tok>(
 	}
 }
 
+fn split_token_pair<'raw>(it: [IndexedToken<'raw>; 2]) -> ([usize; 2], [Token<'raw>; 2]) {
+	([it[0].idx, it[1].idx], [it[0].tok, it[1].tok])
+}
+
 fn find_close<'raw, 'tok>(
 	toks: &'tok [IndexedToken<'raw>],
 	close: Token<'raw>,
@@ -284,125 +328,6 @@ struct FindCloseRet<'raw, 'tok> {
 	remaining: &'tok [IndexedToken<'raw>],
 }
 
-pub trait InterlinkSolver {
-	fn link_title<'n>(&self, name: &'n str) -> Cow<'n, str>;
-	fn link_location<'n>(&self, name: &'n str) -> Cow<'n, str>;
-}
-
-pub struct EchoInterlinkSolver;
-impl InterlinkSolver for EchoInterlinkSolver {
-	fn link_title<'n>(&self, name: &'n str) -> Cow<'n, str> {
-		Cow::Borrowed(name)
-	}
-
-	fn link_location<'n>(&self, name: &'n str) -> Cow<'n, str> {
-		Cow::Borrowed(name)
-	}
-}
-
-//MARK: Tree
-pub struct Document<'raw> {
-	nodes: Vec<TreeNode<'raw>>,
-	solver: Rc<dyn InterlinkSolver>,
-}
-
-impl<'raw> Document<'raw> {
-	pub fn set_interlink_solver(&mut self, solver: Rc<dyn InterlinkSolver>) {
-		self.solver = solver;
-	}
-
-	pub fn html(self) -> String {
-		TreeNode::html_nodes(&self.nodes, Rc::clone(&self.solver))
-	}
-}
-
-#[derive(Clone, Debug, PartialEq)]
-enum TreeNode<'raw> {
-	Text(&'raw str),
-	Paragraph {
-		children: Vec<TreeNode<'raw>>,
-	},
-	Interlink {
-		children: Vec<TreeNode<'raw>>,
-	},
-	StyledText {
-		style: NodeStyle,
-		children: Vec<TreeNode<'raw>>,
-	},
-}
-
-#[derive(Clone, Copy, Debug, PartialEq)]
-enum NodeStyle {
-	Bold,
-	Code,
-	Italic,
-}
-
-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));
-
-				let title = solver.link_title(&txt);
-				let location = solver.link_location(&txt);
-
-				Cow::Owned(format!("<a href=\"{location}\">{title}</a>"))
-			}
-			TreeNode::StyledText { style, children } => {
-				let (open, close) = match style {
-					NodeStyle::Bold => ("<b>", "</b>"),
-					NodeStyle::Code => ("<code>", "</code>"),
-					NodeStyle::Italic => ("<i>", "</i>"),
-				};
-
-				let inner = Self::html_nodes(children.as_slice(), solver);
-				Cow::Owned(format!("{open}{inner}{close}"))
-			}
-		}
-	}
-
-	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())
-			}
-			TreeNode::StyledText { style: _, children } => {
-				Cow::Owned(Self::text_nodes(children, solver))
-			}
-		}
-	}
-
-	fn text_nodes(nodes: &[TreeNode<'raw>], solver: Rc<dyn InterlinkSolver>) -> String {
-		let mut ret = String::new();
-		for node in nodes {
-			let node_txt = &node.text(Rc::clone(&solver));
-			if !node_txt.is_empty() {
-				ret.push_str(node_txt);
-			}
-		}
-
-		ret
-	}
-
-	fn html_nodes(nodes: &[TreeNode<'raw>], solver: Rc<dyn InterlinkSolver>) -> String {
-		let mut ret = String::new();
-		for node in nodes {
-			ret.push_str(&node.html(Rc::clone(&solver)));
-		}
-
-		ret
-	}
-}
-
 //MARK: CollectingCharIter
 struct CollectingCharIter<'raw> {
 	raw: &'raw str,
@@ -474,94 +399,6 @@ impl<'raw> Iterator for CollectingCharIter<'raw> {
 	}
 }
 
-struct Round2Tokens<'raw>(Vec<IndexedToken<'raw>>);
-
-//MARK: Tokens
-#[derive(Copy, Clone, Debug, PartialEq)]
-struct IndexedToken<'raw> {
-	idx: usize,
-	tok: Token<'raw>,
-}
-
-impl<'raw> IndexedToken<'raw> {
-	pub fn new(idx: usize, tok: Token<'raw>) -> Self {
-		Self { idx, tok }
-	}
-}
-
-#[derive(Copy, Clone, Debug, PartialEq)]
-enum Token<'raw> {
-	/// Raw, no markup
-	Text(&'raw str),
-	/// Linebreak
-	Break,
-	/// backtick
-	Code,
-	/// asterisk
-	Italic,
-	/// Double-asterisk
-	Bold,
-	/// backslash
-	Escape,
-	/// Interlink Opening [
-	InterlinkOpen,
-	/// Interlink Closing ]
-	InterlinkClose,
-}
-
-impl<'raw> Token<'raw> {
-	pub fn is_text(&self) -> bool {
-		if let Self::Text(_) = self {
-			true
-		} else {
-			false
-		}
-	}
-
-	pub fn len(&self) -> usize {
-		match self {
-			Self::Text(txt) => txt.len(),
-			Self::Break
-			| Self::Code
-			| Self::Italic
-			| Self::Escape
-			| Self::InterlinkOpen
-			| Self::InterlinkClose => 1,
-			Self::Bold => 2,
-		}
-	}
-
-	pub fn as_str(&self) -> &'raw str {
-		match self {
-			Token::Bold => "**",
-			Token::Break => "\n",
-			Token::Code => "`",
-			Token::Escape => "\\",
-			Token::InterlinkClose => "]",
-			Token::InterlinkOpen => "[",
-			Token::Italic => "*",
-			Token::Text(str) => str,
-		}
-	}
-}
-
-impl<'raw> fmt::Display for Token<'raw> {
-	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-		let str = match self {
-			Token::Bold => "**",
-			Token::Break => "\n",
-			Token::Code => "`",
-			Token::Escape => "\\",
-			Token::InterlinkClose => "]",
-			Token::InterlinkOpen => "[",
-			Token::Italic => "*",
-			Token::Text(str) => str,
-		};
-
-		write!(f, "{str}")
-	}
-}
-
 //MARK: Test
 #[cfg(test)]
 mod test {
@@ -569,7 +406,7 @@ mod test {
 
 	use crate::lineup::{
 		EchoInterlinkSolver, IndexedToken, InterlinkSolver, NodeStyle, Token, TreeNode, process,
-		round1, round2, round3,
+		round1, round2, round3, tree::LinkKind,
 	};
 
 	#[test]
@@ -748,6 +585,38 @@ mod test {
 	}
 
 	#[test]
+	fn round3_interlink() {
+		let raw = "[[link]]";
+		let mut r1 = round1(raw);
+		round2(raw, &mut r1);
+		let tree = round3(raw, &r1);
+
+		assert_eq!(
+			tree,
+			vec![wrap_p!(TreeNode::Interlink {
+				children: vec![TreeNode::Text("link")]
+			})]
+		)
+	}
+
+	#[test]
+	fn round3_link() {
+		let raw = "[named|link]";
+		let mut r1 = round1(raw);
+		round2(raw, &mut r1);
+		let tree = round3(raw, &r1);
+
+		assert_eq!(
+			tree,
+			vec![wrap_p!(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);
@@ -819,6 +688,20 @@ mod test {
 	}
 
 	#[test]
+	fn treenode_html_link() {
+		let node = TreeNode::Link {
+			kind: LinkKind::External,
+			name: Some(vec![TreeNode::Text("dreamy.place")]),
+			children: vec![TreeNode::Text("https://dreamy.place")],
+		};
+
+		let actual = node.html(Rc::new(Solver));
+		let expected = "<a href=\"https://dreamy.place\">dreamy.place</a>";
+
+		assert_eq!(actual, expected)
+	}
+
+	#[test]
 	fn document_html_complex() {
 		let raw = "*italics **bold** after*";
 		let expected = "<i>italics <b>bold</b> after</i>";
diff --git a/markup/src/lineup/token.rs b/markup/src/lineup/token.rs
new file mode 100644
index 0000000..5b61597
--- /dev/null
+++ b/markup/src/lineup/token.rs
@@ -0,0 +1,85 @@
+use core::fmt;
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub(crate) struct IndexedToken<'raw> {
+	pub idx: usize,
+	pub tok: Token<'raw>,
+}
+
+impl<'raw> IndexedToken<'raw> {
+	pub fn new(idx: usize, tok: Token<'raw>) -> Self {
+		Self { idx, tok }
+	}
+}
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub enum Token<'raw> {
+	/// Raw, no markup
+	Text(&'raw str),
+	/// Linebreak
+	Break,
+	/// backtick
+	Code,
+	/// asterisk
+	Italic,
+	/// Double-asterisk
+	Bold,
+	/// backslash
+	Escape,
+	/// Pipe character |
+	Pipe,
+	/// Link Opening [
+	LinkOpen,
+	/// Link Closing ]
+	LinkClose,
+	/// Interlink Open [[
+	InterlinkOpen,
+	/// Interlink CLose ]]
+	InterlinkClose,
+}
+
+impl<'raw> Token<'raw> {
+	pub fn is_text(&self) -> bool {
+		if let Self::Text(_) = self {
+			true
+		} else {
+			false
+		}
+	}
+
+	pub fn len(&self) -> usize {
+		match self {
+			Self::Text(txt) => txt.len(),
+			Self::Break
+			| Self::Code
+			| Self::Italic
+			| Self::Escape
+			| Self::Pipe
+			| Self::LinkOpen
+			| Self::LinkClose => 1,
+			Self::Bold | Self::InterlinkOpen | Self::InterlinkClose => 2,
+		}
+	}
+
+	pub fn as_str(&self) -> &'raw str {
+		match self {
+			Token::Bold => "**",
+			Token::Break => "\n",
+			Token::Code => "`",
+			Token::Escape => "\\",
+			Token::Pipe => "|",
+			Token::LinkClose => "]",
+			Token::LinkOpen => "[",
+			Token::InterlinkOpen => "[[",
+			Token::InterlinkClose => "]]",
+			Token::Italic => "*",
+			Token::Text(str) => str,
+		}
+	}
+}
+
+impl<'raw> fmt::Display for Token<'raw> {
+	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+		write!(f, "{}", self.as_str())
+	}
+}
diff --git a/markup/src/lineup/tree.rs b/markup/src/lineup/tree.rs
new file mode 100644
index 0000000..dcb1cb4
--- /dev/null
+++ b/markup/src/lineup/tree.rs
@@ -0,0 +1,147 @@
+use std::{borrow::Cow, rc::Rc};
+
+use crate::InterlinkSolver;
+
+pub struct Document<'raw> {
+	pub(crate) nodes: Vec<TreeNode<'raw>>,
+	pub(crate) solver: Rc<dyn InterlinkSolver>,
+}
+
+impl<'raw> Document<'raw> {
+	pub fn set_interlink_solver(&mut self, solver: Rc<dyn InterlinkSolver>) {
+		self.solver = solver;
+	}
+
+	pub fn html(self) -> String {
+		TreeNode::html_nodes(&self.nodes, Rc::clone(&self.solver))
+	}
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum TreeNode<'raw> {
+	Text(&'raw str),
+	Paragraph {
+		children: Vec<TreeNode<'raw>>,
+	},
+	Interlink {
+		children: Vec<TreeNode<'raw>>,
+	},
+	Link {
+		kind: LinkKind,
+		name: Option<Vec<TreeNode<'raw>>>,
+		children: Vec<TreeNode<'raw>>,
+	},
+	StyledText {
+		style: NodeStyle,
+		children: Vec<TreeNode<'raw>>,
+	},
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum LinkKind {
+	External,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum NodeStyle {
+	Bold,
+	Code,
+	Italic,
+}
+
+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));
+
+				let title = solver.link_title(&txt);
+				let location = solver.link_location(&txt);
+
+				Cow::Owned(format!("<a href=\"{location}\">{title}</a>"))
+			}
+			TreeNode::Link {
+				kind,
+				name,
+				children,
+			} => {
+				let txt = Self::text_nodes(&children, Rc::clone(&solver));
+
+				let title = match name.as_deref() {
+					None => Cow::Borrowed(txt.as_str()),
+					Some(name) => {
+						let txt = Self::text_nodes(name, Rc::clone(&solver));
+						Cow::Owned(txt)
+					}
+				};
+				let location = txt.as_str();
+
+				Cow::Owned(format!("<a href=\"{location}\">{title}</a>"))
+			}
+			TreeNode::StyledText { style, children } => {
+				let (open, close) = match style {
+					NodeStyle::Bold => ("<b>", "</b>"),
+					NodeStyle::Code => ("<code>", "</code>"),
+					NodeStyle::Italic => ("<i>", "</i>"),
+				};
+
+				let inner = Self::html_nodes(children.as_slice(), solver);
+				Cow::Owned(format!("{open}{inner}{close}"))
+			}
+		}
+	}
+
+	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())
+			}
+			TreeNode::Link {
+				kind: _,
+				name,
+				children,
+			} => match name.as_deref() {
+				None => {
+					let text = Self::text_nodes(children, Rc::clone(&solver));
+					Cow::Owned(solver.link_title(&text).into_owned())
+				}
+				Some(name) => {
+					let text = Self::text_nodes(name, Rc::clone(&solver));
+					Cow::Owned(solver.link_title(&text).into_owned())
+				}
+			},
+			TreeNode::StyledText { style: _, children } => {
+				Cow::Owned(Self::text_nodes(children, solver))
+			}
+		}
+	}
+
+	fn text_nodes(nodes: &[TreeNode<'raw>], solver: Rc<dyn InterlinkSolver>) -> String {
+		let mut ret = String::new();
+		for node in nodes {
+			let node_txt = &node.text(Rc::clone(&solver));
+			if !node_txt.is_empty() {
+				ret.push_str(node_txt);
+			}
+		}
+
+		ret
+	}
+
+	fn html_nodes(nodes: &[TreeNode<'raw>], solver: Rc<dyn InterlinkSolver>) -> String {
+		let mut ret = String::new();
+		for node in nodes {
+			ret.push_str(&node.html(Rc::clone(&solver)));
+		}
+
+		ret
+	}
+}