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 | |
| parent | 178ca3c066a1ef920b112da741625099b6dc07da (diff) | |
massive reorg
Diffstat (limited to 'markup/src')
| -rw-r--r-- | markup/src/lineup/interlinksolver.rs | 17 | ||||
| -rw-r--r-- | markup/src/lineup/mod.rs (renamed from markup/src/lineup.rs) | 365 | ||||
| -rw-r--r-- | markup/src/lineup/token.rs | 85 | ||||
| -rw-r--r-- | markup/src/lineup/tree.rs | 147 |
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 + } +} |
