Something went wrong. Try again.
A git-based task manager
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414#![allow(dead_code)]
use std::{collections::HashSet, str::FromStr};use url::Url;
use crate::workspace::Id;use colored::Colorize;
/// Returns true if the character is a word boundary (whitespace or punctuation)fn is_boundary(c: char) -> bool { c.is_whitespace() || c.is_ascii_punctuation()}
#[derive(Debug, Eq, PartialEq, Clone, Copy)]enum ParserState { // Started by ` =`, terminated by `= Highlight(usize, usize), // Started by ` [`, terminated by `](` Linktext(usize, usize), // Started by `](`, terminated by `) `, must immedately follow a Linktext Link(usize, usize), RawLink(usize, usize), // Started by ` [[`, terminated by `]] ` InternalLink(usize, usize), // Started by ` *`, terminated by `* ` Italics(usize, usize), // Started by ` !`, termianted by `!` Bold(usize, usize), // Started by ` _`, terminated by `_ ` Underline(usize, usize), // Started by ` -`, terminated by `- ` Strikethrough(usize, usize),
// TODO: implement these. // Started by `_ `, terminated by `_` UnorderedList(usize, u8), // Started by `^\w+1.`, terminated by `\n` OrderedList(usize, u8), // Started by `^`````, terminated by a [`ParserOpcode::BlockEnd`] BlockStart(usize), // Started by `$````, is terminal itself. It must appear on its own line and be preceeded by a // `\n` and followed by a `\n` BlockEnd(usize), // Started by ` ``, terminated by `` ` or `\n` InlineBlock(usize, usize), // Started by `^\w+>`, terminated by `\n` Blockquote(usize),}
#[derive(Debug, Eq, PartialEq, Clone)]pub(crate) enum ParsedLink { Internal(Id), External(Url),}
pub(crate) struct ParsedTask { pub(crate) content: String, pub(crate) links: Vec<ParsedLink>,}
impl ParsedTask { pub(crate) fn intenal_links(&self) -> HashSet<Id> { let mut out = HashSet::with_capacity(self.links.len()); for link in &self.links { if let ParsedLink::Internal(id) = link { out.insert(*id); } } out }}
pub(crate) fn parse(s: &str) -> Option<ParsedTask> { let mut state: Vec<ParserState> = Vec::new(); let mut out = String::with_capacity(s.len()); let mut stream = s.char_indices().peekable(); let mut links = Vec::new(); let mut last = '\0'; use ParserState::*; loop { let state_last = state.last().cloned(); match stream.next() { // there will always be an op code in the stack Some((char_pos, c)) => { out.push(c); let end = out.len() - 1; match (last, c, state_last) { ('[', '[', _) => { state.push(InternalLink(end, char_pos)); } (']', ']', Some(InternalLink(il, s_pos))) => { state.pop(); let contents = s.get(s_pos + 1..char_pos - 1)?; if let Ok(id) = Id::from_str(contents) { let linktext = format!( "{}{}", contents.purple(), super_num(links.len() + 1).purple() ); out.replace_range(il - 1..out.len(), &linktext); links.push(ParsedLink::Internal(id)); } else { panic!("Internal link is not a valid id: {contents}"); } } (last, '[', _) if is_boundary(last) => { state.push(Linktext(end, char_pos)); } (']', '(', Some(Linktext(_, _))) => { state.push(Link(end, char_pos)); } (')', c, Some(Link(_, _))) if is_boundary(c) => { // TODO: this needs to be updated to use `s` instead of `out` for position // parsing let linkpos = if let Link(lp, _) = state.pop().unwrap() { lp } else { // remove the linktext state, it is always present. state.pop(); continue; }; let linktextpos = if let Linktext(lt, _) = state.pop().unwrap() { lt } else { continue; }; let linktext = format!( "{}{}", out.get(linktextpos + 1..linkpos - 1)?.blue(), super_num(links.len() + 1).purple() ); let link = out.get(linkpos + 1..end - 1)?; if let Ok(url) = Url::parse(link) { links.push(ParsedLink::External(url)); out.replace_range(linktextpos..end, &linktext); } } ('>', c, Some(RawLink(hl, s_pos))) if is_boundary(c) && s_pos != char_pos - 1 => { state.pop(); let link = s.get(s_pos + 1..char_pos - 1)?; if let Ok(url) = Url::parse(link) { let linktext = format!("{}{}", link.blue(), super_num(links.len() + 1).purple()); links.push(ParsedLink::External(url)); out.replace_range(hl..end, &linktext); } } (last, '<', _) if is_boundary(last) => { state.push(RawLink(end, char_pos)); } ('=', c, Some(Highlight(hl, s_pos))) if is_boundary(c) && s_pos != char_pos - 1 => { state.pop(); out.replace_range( hl..end, &s.get(s_pos + 1..char_pos - 1)?.reversed().to_string(), ); } (last, '=', _) if is_boundary(last) => { state.push(Highlight(end, char_pos)); } (last, '*', _) if is_boundary(last) => { state.push(Italics(end, char_pos)); } ('*', c, Some(Italics(il, s_pos))) if is_boundary(c) && s_pos != char_pos - 1 => { state.pop(); out.replace_range( il..end, &s.get(s_pos + 1..char_pos - 1)?.italic().to_string(), ); } (last, '!', _) if is_boundary(last) => { state.push(Bold(end, char_pos)); } ('!', c, Some(Bold(il, s_pos))) if is_boundary(c) && s_pos != char_pos - 1 => { state.pop(); out.replace_range( il..end, &s.get(s_pos + 1..char_pos - 1)?.bold().to_string(), ); } (last, '_', _) if is_boundary(last) => { state.push(Underline(end, char_pos)); } ('_', c, Some(Underline(il, s_pos))) if is_boundary(c) && s_pos != char_pos - 1 => { state.pop(); out.replace_range( il..end, &s.get(s_pos + 1..char_pos - 1)?.underline().to_string(), ); } (last, '~', _) if is_boundary(last) => { state.push(Strikethrough(end, char_pos)); } ('~', c, Some(Strikethrough(il, s_pos))) if is_boundary(c) && s_pos != char_pos - 1 => { state.pop(); out.replace_range( il..end, &s.get(s_pos + 1..char_pos - 1)?.strikethrough().to_string(), ); } ('`', c, Some(InlineBlock(hl, s_pos))) if is_boundary(c) && s_pos != char_pos - 1 => { out.replace_range( hl..end, &s.get(s_pos + 1..char_pos - 1)?.green().to_string(), ); } (last, '`', _) if is_boundary(last) => { state.push(InlineBlock(end, char_pos)); } _ => (), } if c == '\n' || c == '\r' { state.clear(); } last = c; } None => break, } } Some(ParsedTask { content: out, links, })}
/// Converts a unsigned integer into a superscripted stringfn super_num(num: usize) -> String { let num_str = num.to_string(); let mut out = String::with_capacity(num_str.len()); for char in num_str.chars() { out.push(match char { '0' => '⁰', '1' => '¹', '2' => '²', '3' => '³', '4' => '⁴', '5' => '⁵', '6' => '⁶', '7' => '⁷', '8' => '⁸', '9' => '⁹', _ => unreachable!(), }); } out}
#[cfg(test)]mod test { use super::*; #[test] fn test_highlight() { let input = "hello =world=\n"; let output = parse(input).expect("parse to work"); assert_eq!("hello \u{1b}[7mworld\u{1b}[0m\n", output.content); }
#[test] fn test_highlight_bad() { let input = "hello =world\n"; let output = parse(input).expect("parse to work"); assert_eq!(input, output.content); }
#[test] fn test_link() { let input = "hello [world](https://ngp.computer)\n"; let output = parse(input).expect("parse to work"); assert_eq!( &[ParsedLink::External( Url::parse("https://ngp.computer").unwrap() )], output.links.as_slice() ); assert_eq!( "hello \u{1b}[34mworld\u{1b}[0m\u{1b}[35m¹\u{1b}[0m\n", output.content ); }
#[test] fn test_link_no_terminal_link() { let input = "hello [world](https://ngp.computer\n"; let output = parse(input).expect("parse to work"); assert!(output.links.is_empty()); assert_eq!(input, output.content); } #[test] fn test_link_bad_no_start_link() { let input = "hello [world]https://ngp.computer)\n"; let output = parse(input).expect("parse to work"); assert!(output.links.is_empty()); assert_eq!(input, output.content); } #[test] fn test_link_bad_no_link() { let input = "hello [world]\n"; let output = parse(input).expect("parse to work"); assert!(output.links.is_empty()); assert_eq!(input, output.content); }
#[test] fn test_internal_link_good() { let input = "hello [[tsk-123]]\n"; let output = parse(input).expect("parse to work"); assert_eq!(&[ParsedLink::Internal(Id(123))], output.links.as_slice()); assert_eq!( "hello \u{1b}[35mtsk-123\u{1b}[0m\u{1b}[35m¹\u{1b}[0m\n", output.content ); }
#[test] fn test_internal_link_bad() { let input = "hello [[tsk-123"; let output = parse(input).expect("parse to work"); assert!(output.links.is_empty()); assert_eq!(input, output.content); }
#[test] fn test_italics() { let input = "hello *world*\n"; let output = parse(input).expect("parse to work"); assert_eq!("hello \u{1b}[3mworld\u{1b}[0m\n", output.content); }
#[test] fn test_italics_bad() { let input = "hello *world"; let output = parse(input).expect("parse to work"); assert_eq!(input, output.content); }
#[test] fn test_bold() { let input = "hello !world!\n"; let output = parse(input).expect("parse to work"); assert_eq!("hello \u{1b}[1mworld\u{1b}[0m\n", output.content); }
#[test] fn test_bold_bad() { let input = "hello !world\n"; let output = parse(input).expect("parse to work"); assert_eq!(input, output.content); }
#[test] fn test_underline() { let input = "hello _world_\n"; let output = parse(input).expect("parse to work"); assert_eq!("hello \u{1b}[4mworld\u{1b}[0m\n", output.content); }
#[test] fn test_underline_bad() { let input = "hello _world\n"; let output = parse(input).expect("parse to work"); assert_eq!(input, output.content); }
#[test] fn test_strikethrough() { let input = "hello ~world~\n"; let output = parse(input).expect("parse to work"); assert_eq!("hello \u{1b}[9mworld\u{1b}[0m\n", output.content); }
#[test] fn test_strikethrough_bad() { let input = "hello ~world\n"; let output = parse(input).expect("parse to work"); assert_eq!(input, output.content); }
#[test] fn test_inlineblock() { let input = "hello `world`\n"; let output = parse(input).expect("parse to work"); assert_eq!("hello \u{1b}[32mworld\u{1b}[0m\n", output.content); }
#[test] fn test_inlineblock_bad() { let input = "hello `world\n"; let output = parse(input).expect("parse to work"); assert_eq!(input, output.content); }
#[test] fn test_multiple_styles() { let input = "hello *italic* ~strikethrough~ !bold!\n"; let output = parse(input).expect("parse to work"); assert_eq!( "hello \u{1b}[3mitalic\u{1b}[0m \u{1b}[9mstrikethrough\u{1b}[0m \u{1b}[1mbold\u{1b}[0m\n", output.content ); }}