diff options
| author | gennyble <gen@nyble.dev> | 2026-09-26 21:00:51 -0500 |
|---|---|---|
| committer | gennyble <gen@nyble.dev> | 2026-09-26 21:00:51 -0500 |
| commit | 0baa29ab3d1bdb13d987a221ac801250ac149dfe (patch) | |
| tree | 83177115a86a1ca48613e8cfd651a0a295fc7998 /markup/src | |
| parent | 92ac66f53f9fcbe758123bb8900f0214d5892da9 (diff) | |
coloring! also no more lineup multiline
Diffstat (limited to 'markup/src')
| -rw-r--r-- | markup/src/breakdown.rs | 1 | ||||
| -rw-r--r-- | markup/src/lineup/mod.rs | 302 | ||||
| -rw-r--r-- | markup/src/lineup/token.rs | 18 | ||||
| -rw-r--r-- | markup/src/lineup/tree.rs | 19 |
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)) + } } } |
