diff options
| author | gennyble <gen@nyble.dev> | 2026-09-21 14:23:07 -0500 |
|---|---|---|
| committer | gennyble <gen@nyble.dev> | 2026-09-21 14:23:07 -0500 |
| commit | be872aa0cd61ea16b95eddcd68c6a72f0fe3af96 (patch) | |
| tree | 8935da5f9639b939a0e5ecd2bba3072d71d602bc /markup/src/lineup | |
| parent | 178ca3c066a1ef920b112da741625099b6dc07da (diff) | |
massive reorg
Diffstat (limited to 'markup/src/lineup')
| -rw-r--r-- | markup/src/lineup/interlinksolver.rs | 17 | ||||
| -rw-r--r-- | markup/src/lineup/mod.rs | 742 | ||||
| -rw-r--r-- | markup/src/lineup/token.rs | 85 | ||||
| -rw-r--r-- | markup/src/lineup/tree.rs | 147 |
4 files changed, 991 insertions, 0 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/mod.rs b/markup/src/lineup/mod.rs new file mode 100644 index 0000000..0e1ecce --- /dev/null +++ b/markup/src/lineup/mod.rs @@ -0,0 +1,742 @@ +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); + 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, + solver, + } +} + +//MARK: ✨ Passes ✨ + +/// Round 1 tokenizes to `Token` but does not produce `Bold` or properly +/// run `Token::Escape`. round2() is the next step +fn round1<'raw>(raw: &'raw str) -> Vec<IndexedToken<'raw>> { + let mut chars = CollectingCharIter::from(raw); + + let mut toks = vec![]; + loop { + let (idx, ch) = match chars.next() { + Some(pair) => pair, + None => { + chars.append_slice(None, &mut toks); + break; + } + }; + + match ch { + '\\' => { + chars.append_slice(Some(idx), &mut toks); + toks.push(IndexedToken::new(idx, Token::Escape)); + } + '`' => { + chars.append_slice(Some(idx), &mut toks); + toks.push(IndexedToken::new(idx, Token::Code)); + } + '*' => { + 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::LinkOpen)); + } + ']' => { + chars.append_slice(Some(idx), &mut toks); + toks.push(IndexedToken::new(idx, Token::LinkClose)); + } + '\r' => { + 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(); + } + } + } + + toks +} + +/// Folds two subsequent `Token::Itlalic` into `Token::Bold` and correctly maps +/// `Token::Escape`. Additionally, combines successive `Token::Text` into +/// single slices. +fn round2<'raw>(raw: &'raw str, toks: &mut Vec<IndexedToken<'raw>>) { + let mut idx = 0; + loop { + // We alawys want there to be a token after our idx + if idx >= toks.len() - 1 { + break; + } + + let indicies: [usize; 2] = [toks[idx].idx, toks[idx + 1].idx]; + let pattern: [Token<'raw>; 2] = [toks[idx].tok, toks[idx + 1].tok]; + + match pattern { + [Token::Italic, Token::Italic] => { + toks[idx] = IndexedToken { + idx: indicies[0], + tok: Token::Bold, + }; + 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; + } + _ => (), + } + + idx += 1; + } + + // Collapse subsequent Token::Text and remove Token::Escape + idx = 0; + loop { + if idx >= toks.len() - 1 { + break; + } + + let (pair_idx, pair) = split_token_pair([toks[idx], toks[idx + 1]]); + + if pair[0] == Token::Escape { + if pair[1].is_text() { + // Erroneous escape, remove it and leave the backslash in the text + let tok_idx = pair_idx[0]; + let len = 1 + pair[1].len(); + let tok = Token::Text(&raw[tok_idx..tok_idx + len]); + toks[idx + 1] = IndexedToken::new(tok_idx, tok); + toks.remove(idx); + idx = idx.saturating_sub(1); + continue; + } else { + toks[idx + 1] = IndexedToken { + idx: pair_idx[1], + tok: Token::Text(&raw[pair_idx[1]..pair_idx[1] + pair[1].len()]), + }; + } + } + + 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; + } + + idx = 0; + loop { + if toks[idx].tok == Token::Escape { + toks.remove(idx); + + // Don't increment idx, we removed an element so the vec slid down + continue; + } + + idx += 1; + if idx == toks.len() { + break; + } + } +} + +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 +} + +fn round3_intralevel<'raw, 'tok>( + raw: &'raw str, + toks: &'tok [IndexedToken<'raw>], +) -> Vec<TreeNode<'raw>> { + let mut doc = vec![]; + + let mut curr = &toks[..]; + loop { + if curr.is_empty() { + break doc; + } + + 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 { + decay!() + } + + match tok.tok { + Token::Text(_) | Token::Pipe | Token::LinkClose | Token::InterlinkClose => decay!(), + Token::Break | Token::Escape => unreachable!(), + Token::InterlinkOpen => match find_close(&curr[1..], Token::InterlinkClose) { + None => decay!(), + Some(FindCloseRet { group, remaining }) => { + let inner = round3_intralevel(raw, group); + curr = remaining; + doc.push(TreeNode::Interlink { children: inner }); + continue; + } + }, + 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; + + let style = match tok.tok { + Token::Bold => NodeStyle::Bold, + Token::Code => NodeStyle::Code, + Token::Italic => NodeStyle::Italic, + _ => unreachable!(), + }; + + doc.push(TreeNode::StyledText { + style, + children: inner, + }); + continue; + } + }, + } + } +} + +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>, +) -> Option<FindCloseRet<'raw, 'tok>> { + match toks.iter().position(|tok| tok.tok.eq(&close)) { + None => None, + Some(idx) => { + let group = &toks[..idx]; + let remaining = &toks[idx + 1..]; + + Some(FindCloseRet { group, remaining }) + } + } +} + +struct FindCloseRet<'raw, 'tok> { + group: &'tok [IndexedToken<'raw>], + remaining: &'tok [IndexedToken<'raw>], +} + +//MARK: CollectingCharIter +struct CollectingCharIter<'raw> { + raw: &'raw str, + chars: Enumerate<Chars<'raw>>, + raw_slice_start: Option<usize>, + + last: Option<(usize, char)>, + current: Option<(usize, char)>, +} + +impl<'raw> CollectingCharIter<'raw> { + pub fn take_slice(&mut self, end: Option<usize>) -> Option<IndexedToken<'raw>> { + let end = if let Some(end_idx) = end { + end_idx + } else { + self.raw.len() + }; + + if let Some(start) = self.raw_slice_start.take() { + Some(IndexedToken { + idx: start, + tok: Token::Text(&self.raw[start..end]), + }) + } else { + None + } + } + + pub fn append_slice(&mut self, end: Option<usize>, tokens: &mut Vec<IndexedToken<'raw>>) { + if let Some(slice) = self.take_slice(end) { + tokens.push(slice); + } + } + + pub fn set_raw_if_empty(&mut self) { + if self.raw_slice_start.is_none() { + if let Some((start, _)) = self.current { + self.raw_slice_start = Some(start); + } + } + } + + pub fn last(&self) -> Option<(usize, char)> { + self.last + } +} + +impl<'raw> From<&'raw str> for CollectingCharIter<'raw> { + fn from(raw: &'raw str) -> Self { + Self { + raw: raw, + chars: raw.chars().enumerate(), + raw_slice_start: None, + + last: None, + current: None, + } + } +} + +impl<'raw> Iterator for CollectingCharIter<'raw> { + type Item = (usize, char); + + fn next(&mut self) -> Option<Self::Item> { + let next = self.chars.next(); + self.last = self.current.take(); + self.current = next; + next + } +} + +//MARK: Test +#[cfg(test)] +mod test { + use std::{borrow::Cow, rc::Rc}; + + use crate::lineup::{ + EchoInterlinkSolver, IndexedToken, InterlinkSolver, NodeStyle, Token, TreeNode, process, + round1, round2, round3, tree::LinkKind, + }; + + #[test] + fn round1_textonly() { + let raw = "Raw Text!"; + let r1 = round1(raw); + + assert_eq!(r1[0].tok, Token::Text("Raw Text!")) + } + + #[test] + fn round1_single_asterisk() { + let raw = "Raw*Text!"; + let r1 = round1(raw); + + assert_eq!(r1[0].tok, Token::Text("Raw")); + assert_eq!(r1[0].idx, 0); + + assert_eq!(r1[1].tok, Token::Italic); + assert_eq!(r1[1].idx, 3); + + assert_eq!(r1[2].tok, Token::Text("Text!")); + assert_eq!(r1[2].idx, 4); + } + + #[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![ + IndexedToken::new(0, Token::Italic), + IndexedToken::new(1, Token::Italic), + ]; + + round2(raw, &mut r1); + + assert_eq!(r1, vec![IndexedToken::new(0, Token::Bold)]); + } + + #[test] + fn round2_finds_escape() { + // 012 345 + let raw = "\\* \\**"; + let mut r1 = vec![ + IndexedToken::new(0, Token::Escape), + IndexedToken::new(1, Token::Italic), + IndexedToken::new(2, Token::Text(&raw[2..3])), + IndexedToken::new(3, Token::Escape), + IndexedToken::new(4, Token::Italic), + IndexedToken::new(5, Token::Italic), + ]; + + round2(raw, &mut r1); + + assert_eq!( + r1, + vec![ + IndexedToken::new(1, Token::Text("* ")), + IndexedToken::new(4, Token::Text("**")) + ] + ) + } + + #[test] + fn round2_erroneous_escape() { + let raw = "\\wrong escape"; + let mut r1 = round1(raw); + round2(raw, &mut r1); + + assert_eq!(r1[0].tok, Token::Text("\\wrong escape")) + } + + #[test] + fn round2_middle_erreoneous_escape() { + let raw = "wrong \\escape"; + let mut r1 = round1(raw); + round2(raw, &mut r1); + + assert_eq!( + r1, + vec![IndexedToken::new(0, Token::Text("wrong \\escape"))] + ) + } + + #[test] + fn round2_complex() { + // 0 1 2 + // 01234567890123456789 0123456789 + let raw = "Start **bold** and \\* escape"; + let mut r1 = round1(raw); + round2(raw, &mut r1); + + assert_eq!( + r1, + vec![ + IndexedToken::new(0, Token::Text("Start ")), + 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")) + ] + ) + } + + macro_rules! wrap_p { + ($some:expr) => { + TreeNode::Paragraph { + children: vec![$some], + } + }; + } + + #[test] + fn round3_textonly() { + let raw = "Raw text!"; + let mut r1 = round1(raw); + round2(raw, &mut r1); + + let tree = round3(raw, &r1); + + assert_eq!(tree, vec![wrap_p!(TreeNode::Text(raw))]); + } + + #[test] + fn round3_italics() { + let raw = "*italisized!*"; + let mut r1 = round1(raw); + round2(raw, &mut r1); + + let tree = round3(raw, &r1); + + assert_eq!( + tree, + vec![wrap_p!(TreeNode::StyledText { + style: NodeStyle::Italic, + children: vec![TreeNode::Text("italisized!")] + })] + ) + } + + #[test] + fn round3_bold_in_italics() { + let raw = "*italics **bold** after*"; + let mut r1 = round1(raw); + round2(raw, &mut r1); + + let tree = round3(raw, &r1); + + assert_eq!( + tree, + vec![wrap_p!(TreeNode::StyledText { + style: NodeStyle::Italic, + children: vec![ + TreeNode::Text("italics "), + TreeNode::StyledText { + style: NodeStyle::Bold, + children: vec![TreeNode::Text("bold")] + }, + TreeNode::Text(" after") + ] + })] + ) + } + + #[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); + let actual = node.html(rcsolve); + let expected = "Raw Text!"; + + assert_eq!(actual, expected) + } + + #[test] + fn treenode_html_paragraph() { + let node = TreeNode::Paragraph { + children: vec![TreeNode::Text("Para 1"), TreeNode::Text(" Para 2")], + }; + let rcsolve = Rc::new(EchoInterlinkSolver); + let actual = node.html(rcsolve); + let expected = "<p>Para 1 Para 2</p>"; + + assert_eq!(actual, expected) + } + + #[test] + fn treenode_html_italics() { + let node = TreeNode::StyledText { + style: NodeStyle::Italic, + children: vec![TreeNode::Text("italics!")], + }; + let rcsolve = Rc::new(EchoInterlinkSolver); + let actual = node.html(rcsolve); + let expected = "<i>italics!</i>"; + + assert_eq!(actual, expected) + } + + #[test] + fn treenode_html_interlink() { + let node = TreeNode::Interlink { + children: vec![TreeNode::Text("link!")], + }; + let rcsolve = Rc::new(EchoInterlinkSolver); + let actual = node.html(rcsolve); + let expected = "<a href=\"link!\">link!</a>"; + + assert_eq!(actual, expected) + } + + struct Solver; + impl InterlinkSolver for Solver { + fn link_title<'n>(&self, name: &'n str) -> Cow<'n, str> { + Cow::Owned(name.to_uppercase()) + } + + fn link_location<'n>(&self, name: &'n str) -> Cow<'n, str> { + Cow::Owned(format!("/quark/{name}")) + } + } + + #[test] + fn treenode_html_interlink_with_solver() { + let node = TreeNode::Interlink { + children: vec![TreeNode::Text("link!")], + }; + + let solver = Rc::new(Solver); + let actual = node.html(solver); + let expected = "<a href=\"/quark/link!\">LINK!</a>"; + + assert_eq!(actual, expected) + } + + #[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>"; + let rcsolve = Rc::new(EchoInterlinkSolver); + let doc = process(raw, rcsolve); + let html = doc.html(); + + 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 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 + } +} |
