A file-based task manager

WIP: more parser stuff

+169 -43
+169 -43
src/task.rs
··· 1 1 #![allow(dead_code)] 2 2 use url::Url; 3 3 4 - use crate::workspace::Id; 4 + use crate::{errors::Error, workspace::Id}; 5 + use colored::Colorize; 5 6 6 - /// An AST node parsed from the plain-text representation of 7 - enum TaskASTNode<'n> { 8 - /// Unmodified text 9 - Plain(&'n str), 10 - /// Text that has been highlighted using =test= syntax. 11 - Highlight(Box<TaskASTNode<'n>>), 12 - /// A standard Markdown-style link 13 - Link { 14 - text: Box<TaskASTNode<'n>>, 15 - to: Url, 16 - }, 17 - /// An internal link to another task using custom [[tsk-id]] syntax 18 - InternalLink(Id), 19 - /// Italicized text using Markdown *text* syntax. 20 - Italics(Box<TaskASTNode<'n>>), 21 - /// Bolded text using !text! syntax. 22 - Bold(Box<TaskASTNode<'n>>), 23 - /// Underlined text using custom _text_ syntax. 24 - Underline(Box<TaskASTNode<'n>>), 25 - /// Strikethrough using -text- syntax 26 - Strikethrough(Box<TaskASTNode<'n>>), 27 - /// Unordered list using Markdown * list-item syntax 28 - UnorderedList(Vec<TaskASTNode<'n>>), 29 - /// Ordered list using Markdown 1. list-item syntax 30 - OrderedList(Vec<TaskASTNode<'n>>), 31 - /// Literal block using markdown triple-backtick syntax. 32 - Block { 33 - /// An optional syntax specifier. This *may* be used to apply syntax formatting to contents 34 - /// in the future 35 - syntax: Option<&'n str>, 36 - /// The verbatim content of the block 37 - content: &'n str, 38 - }, 39 - /// Literal block using markdown single-backtick syntax. 40 - InlineBlock(&'n str), 41 - /// Blockquotes using Markdown > quote syntax 42 - Blockquote(&'n str), 7 + #[derive(Debug, Eq, PartialEq)] 8 + enum ParserOpcode { 9 + // Started by ` =`, terminated by `= 10 + Highlight(usize), 11 + // Started by ` [`, terminated by `](` 12 + Linktext(usize), 13 + // Started by `](`, terminated by `) `, must immedately follow a Linktext 14 + Link(usize), 15 + // Used to signal to the parser that the Linktext parsed properly and we should parse the 16 + // subsequent ( character as a 17 + LinkJoin, 18 + // Started by ` [[`, terminated by `]] ` 19 + InternalLink(usize), 20 + // Started by ` *`, terminated by `* ` 21 + Italics(usize), 22 + // Started by ` !`, termianted by `!` 23 + Bold(usize), 24 + // Started by ` _`, terminated by `_ ` 25 + Underline(usize), 26 + // Started by ` -`, terminated by `- ` 27 + Strikethrough(usize), 28 + // Started by `_ `, terminated by `_` 29 + UnorderedList(usize, u8), 30 + // Started by `^\w+1.`, terminated by `\n` 31 + OrderedList(usize, u8), 32 + // Started by `^`````, terminated by a [`ParserOpcode::BlockEnd`] 33 + BlockStart(usize), 34 + // Started by `$````, is terminal itself. It must appear on its own line and be preceeded by a 35 + // `\n` and followed by a `\n` 36 + BlockEnd(usize), 37 + // Started by ` ``, terminated by `` ` or `\n` 38 + InlineBlock(usize), 39 + // Started by `^\w+>`, terminated by `\n` 40 + Blockquote(usize), 43 41 } 44 42 45 - impl<'i> TaskASTNode<'i> { 46 - fn parse(s: &'i String) -> Result<Vec<TaskASTNode<'i>>, String> { 47 - let i = 0; 48 - let mut roots: Vec<TaskASTNode<'i>> = Vec::new(); 43 + pub(crate) struct ParsedTask { 44 + content: String, 45 + outgoing_internal_links: Vec<Id>, 46 + links: Vec<Url>, 47 + } 49 48 50 - todo!(); 49 + pub(crate) fn parse(s: &str) -> Option<ParsedTask> { 50 + let mut out = String::with_capacity(s.len()); 51 + let mut ops: Vec<ParserOpcode> = Vec::new(); 52 + let mut stream = s.char_indices().peekable(); 53 + let outgoing_internal_links = Vec::new(); 54 + let mut links = Vec::new(); 55 + loop { 56 + use ParserOpcode::*; 57 + match stream.next() { 58 + // there will always be an op code in the stack 59 + Some((pos, c)) => match dbg!((ops.last(), c)) { 60 + // Highlight terminal 61 + (Some(Highlight(start)), '=') => { 62 + out.push_str(&s[start + 1..=pos - 1].reversed().to_string()); 63 + // reduce 64 + ops.pop(); 65 + } 66 + // Highlight start 67 + (op, '=') => { 68 + ops.push(Highlight(pos)); 69 + } 70 + (Some(Linktext(start)), ']') => match stream.peek() { 71 + Some((_, '(')) => { 72 + out.push_str(&s[start + 1..=pos - 1].bright_blue().underline().to_string()); 73 + ops.pop(); 74 + ops.push(LinkJoin) 75 + } 76 + // Terminal for internal link 77 + Some((_, ']')) => { 78 + out.push_str(&s[start + 1..=pos - 1].green().bold().to_string()); 79 + ops.pop(); 80 + } 81 + _ => (), 82 + }, 83 + (Some(Link(start)), ')') => { 84 + if let Ok(uri) = Url::parse(&s[start + 1..=pos - 1]) { 85 + links.push(uri); 86 + } 87 + } 88 + (op, '[') => { 89 + if let Some(op) = op { 90 + ops.push(op); 91 + } 92 + ops.push(Linktext(pos)); 93 + } 94 + (Some(LinkJoin), '(') => { 95 + ops.push(Link(pos)); 96 + } 97 + (None | Some(_), c) => out.push(c), 98 + }, 99 + None => match ops.pop() { 100 + Some( 101 + Plain(start) | Highlight(start) | Linktext(start) | Link(start) 102 + | InternalLink(start) | Italics(start) | Bold(start) | Underline(start) 103 + | Strikethrough(start), 104 + ) => { 105 + // We have an 106 + return None; 107 + } 108 + None => { 109 + break; 110 + } 111 + Some(LinkJoin) => unreachable!(), 112 + Some(UnorderedList(_, _)) => todo!(), 113 + Some(OrderedList(_, _)) => todo!(), 114 + Some(BlockStart(_)) => todo!(), 115 + Some(BlockEnd(_)) => todo!(), 116 + Some(InlineBlock(_)) => todo!(), 117 + Some(Blockquote(_)) => todo!(), 118 + }, 119 + } 120 + } 121 + Some(ParsedTask { 122 + content: out, 123 + outgoing_internal_links, 124 + links, 125 + }) 126 + } 127 + 128 + #[cfg(test)] 129 + mod test { 130 + use super::*; 131 + #[test] 132 + fn test_highlight() { 133 + let input = "hello =world="; 134 + let output = parse(input).expect("parse to work"); 135 + assert_eq!("hello \u{1b}[7mworld\u{1b}[0m", output.content); 136 + } 137 + 138 + #[test] 139 + fn test_highlight_bad() { 140 + let input = "hello =world"; 141 + let output = parse(input).expect("parse to work"); 142 + assert_eq!("hello =world", output.content); 143 + } 144 + 145 + #[test] 146 + fn test_link() { 147 + let input = "hello [world](https://ngp.computer)"; 148 + let output = parse(input).expect("parse to work"); 149 + assert_eq!( 150 + &[Url::parse("https://ngp.computer").unwrap()], 151 + output.links.as_slice() 152 + ); 153 + assert_eq!("hello \u{1b}[4;94mworld\u{1b}[0m", output.content); 154 + } 155 + 156 + #[ignore = "Known styling bug"] 157 + #[test] 158 + fn test_link_no_terminal_link() { 159 + let input = "hello [world](https://ngp.computer"; 160 + let output = parse(input).expect("parse to work"); 161 + assert!(output.links.len() == 0); 162 + assert_eq!(input, output.content); 163 + } 164 + #[test] 165 + fn test_link_bad_no_start_link() { 166 + let input = "hello [world]https://ngp.computer)"; 167 + let output = parse(input).expect("parse to work"); 168 + assert!(output.links.len() == 0); 169 + assert_eq!(input, output.content); 170 + } 171 + #[test] 172 + fn test_link_bad_no_link() { 173 + let input = "hello [world]"; 174 + let output = parse(input).expect("parse to work"); 175 + assert!(output.links.len() == 0); 176 + assert_eq!(input, output.content); 51 177 } 52 178 }