A filesystem-based task manager
at master 410 lines 15 kB view raw
1#![allow(dead_code)] 2 3use std::{collections::HashSet, str::FromStr}; 4use url::Url; 5 6use crate::workspace::Id; 7use colored::Colorize; 8 9#[derive(Debug, Eq, PartialEq, Clone, Copy)] 10enum ParserState { 11 // Started by ` =`, terminated by `= 12 Highlight(usize, usize), 13 // Started by ` [`, terminated by `](` 14 Linktext(usize, usize), 15 // Started by `](`, terminated by `) `, must immedately follow a Linktext 16 Link(usize, usize), 17 RawLink(usize, usize), 18 // Started by ` [[`, terminated by `]] ` 19 InternalLink(usize, usize), 20 // Started by ` *`, terminated by `* ` 21 Italics(usize, usize), 22 // Started by ` !`, termianted by `!` 23 Bold(usize, usize), 24 // Started by ` _`, terminated by `_ ` 25 Underline(usize, usize), 26 // Started by ` -`, terminated by `- ` 27 Strikethrough(usize, usize), 28 29 // TODO: implement these. 30 // Started by `_ `, terminated by `_` 31 UnorderedList(usize, u8), 32 // Started by `^\w+1.`, terminated by `\n` 33 OrderedList(usize, u8), 34 // Started by `^`````, terminated by a [`ParserOpcode::BlockEnd`] 35 BlockStart(usize), 36 // Started by `$````, is terminal itself. It must appear on its own line and be preceeded by a 37 // `\n` and followed by a `\n` 38 BlockEnd(usize), 39 // Started by ` ``, terminated by `` ` or `\n` 40 InlineBlock(usize, usize), 41 // Started by `^\w+>`, terminated by `\n` 42 Blockquote(usize), 43} 44 45#[derive(Debug, Eq, PartialEq, Clone)] 46pub(crate) enum ParsedLink { 47 Internal(Id), 48 External(Url), 49} 50 51pub(crate) struct ParsedTask { 52 pub(crate) content: String, 53 pub(crate) links: Vec<ParsedLink>, 54} 55 56impl ParsedTask { 57 pub(crate) fn intenal_links(&self) -> HashSet<Id> { 58 let mut out = HashSet::with_capacity(self.links.len()); 59 for link in &self.links { 60 if let ParsedLink::Internal(id) = link { 61 out.insert(*id); 62 } 63 } 64 out 65 } 66} 67 68pub(crate) fn parse(s: &str) -> Option<ParsedTask> { 69 let mut state: Vec<ParserState> = Vec::new(); 70 let mut out = String::with_capacity(s.len()); 71 let mut stream = s.char_indices().peekable(); 72 let mut links = Vec::new(); 73 let mut last = '\0'; 74 use ParserState::*; 75 loop { 76 let state_last = state.last().cloned(); 77 match stream.next() { 78 // there will always be an op code in the stack 79 Some((char_pos, c)) => { 80 out.push(c); 81 let end = out.len() - 1; 82 match (last, c, state_last) { 83 ('[', '[', _) => { 84 state.push(InternalLink(end, char_pos)); 85 } 86 (']', ']', Some(InternalLink(il, s_pos))) => { 87 state.pop(); 88 let contents = s.get(s_pos + 1..char_pos - 1)?; 89 if let Ok(id) = Id::from_str(contents) { 90 let linktext = format!( 91 "{}{}", 92 contents.purple(), 93 super_num(links.len() + 1).purple() 94 ); 95 out.replace_range(il - 1..out.len(), &linktext); 96 links.push(ParsedLink::Internal(id)); 97 } else { 98 panic!("Internal link is not a valid id: {contents}"); 99 } 100 } 101 (' ' | '\r' | '\n', '[', _) => { 102 state.push(Linktext(end, char_pos)); 103 } 104 (']', '(', Some(Linktext(_, _))) => { 105 state.push(Link(end, char_pos)); 106 } 107 (')', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Link(_, _))) => { 108 // TODO: this needs to be updated to use `s` instead of `out` for position 109 // parsing 110 let linkpos = if let Link(lp, _) = state.pop().unwrap() { 111 lp 112 } else { 113 // remove the linktext state, it is always present. 114 state.pop(); 115 continue; 116 }; 117 let linktextpos = if let Linktext(lt, _) = state.pop().unwrap() { 118 lt 119 } else { 120 continue; 121 }; 122 let linktext = format!( 123 "{}{}", 124 out.get(linktextpos + 1..linkpos - 1)?.blue(), 125 super_num(links.len() + 1).purple() 126 ); 127 let link = out.get(linkpos + 1..end - 1)?; 128 if let Ok(url) = Url::parse(link) { 129 links.push(ParsedLink::External(url)); 130 out.replace_range(linktextpos..end, &linktext); 131 } 132 } 133 ('>', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(RawLink(hl, s_pos))) 134 if s_pos != char_pos - 1 => 135 { 136 state.pop(); 137 let link = s.get(s_pos + 1..char_pos - 1)?; 138 if let Ok(url) = Url::parse(link) { 139 let linktext = 140 format!("{}{}", link.blue(), super_num(links.len() + 1).purple()); 141 links.push(ParsedLink::External(url)); 142 out.replace_range(hl..end, &linktext); 143 } 144 } 145 (' ' | '\r' | '\n', '<', _) => { 146 state.push(RawLink(end, char_pos)); 147 } 148 ('=', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Highlight(hl, s_pos))) 149 if s_pos != char_pos - 1 => 150 { 151 state.pop(); 152 out.replace_range( 153 hl..end, 154 &s.get(s_pos + 1..char_pos - 1)?.reversed().to_string(), 155 ); 156 } 157 (' ' | '\r' | '\n', '=', _) => { 158 state.push(Highlight(end, char_pos)); 159 } 160 (' ' | '\r' | '\n', '*', _) => { 161 state.push(Italics(end, char_pos)); 162 } 163 ('*', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Italics(il, s_pos))) 164 if s_pos != char_pos - 1 => 165 { 166 state.pop(); 167 out.replace_range( 168 il..end, 169 &s.get(s_pos + 1..char_pos - 1)?.italic().to_string(), 170 ); 171 } 172 (' ' | '\r' | '\n', '!', _) => { 173 state.push(Bold(end, char_pos)); 174 } 175 ('!', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Bold(il, s_pos))) 176 if s_pos != char_pos - 1 => 177 { 178 state.pop(); 179 out.replace_range( 180 il..end, 181 &s.get(s_pos + 1..char_pos - 1)?.bold().to_string(), 182 ); 183 } 184 (' ' | '\r' | '\n', '_', _) => { 185 state.push(Underline(end, char_pos)); 186 } 187 ('_', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Underline(il, s_pos))) 188 if s_pos != char_pos - 1 => 189 { 190 state.pop(); 191 out.replace_range( 192 il..end, 193 &s.get(s_pos + 1..char_pos - 1)?.underline().to_string(), 194 ); 195 } 196 (' ' | '\r' | '\n', '~', _) => { 197 state.push(Strikethrough(end, char_pos)); 198 } 199 ('~', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(Strikethrough(il, s_pos))) 200 if s_pos != char_pos - 1 => 201 { 202 state.pop(); 203 out.replace_range( 204 il..end, 205 &s.get(s_pos + 1..char_pos - 1)?.strikethrough().to_string(), 206 ); 207 } 208 ('`', ' ' | '\n' | '\r' | '.' | '!' | '?', Some(InlineBlock(hl, s_pos))) 209 if s_pos != char_pos - 1 => 210 { 211 out.replace_range( 212 hl..end, 213 &s.get(s_pos + 1..char_pos - 1)?.green().to_string(), 214 ); 215 } 216 (' ' | '\n' | '\r' | '.' | '!' | '?', '`', _) => { 217 state.push(InlineBlock(end, char_pos)); 218 } 219 _ => (), 220 } 221 if c == '\n' || c == '\r' { 222 state.clear(); 223 } 224 last = c; 225 } 226 None => break, 227 } 228 } 229 Some(ParsedTask { 230 content: out, 231 links, 232 }) 233} 234 235/// Converts a unsigned integer into a superscripted string 236fn super_num(num: usize) -> String { 237 let num_str = num.to_string(); 238 let mut out = String::with_capacity(num_str.len()); 239 for char in num_str.chars() { 240 out.push(match char { 241 '0' => '⁰', 242 '1' => '¹', 243 '2' => '²', 244 '3' => '³', 245 '4' => '⁴', 246 '5' => '⁵', 247 '6' => '⁶', 248 '7' => '⁷', 249 '8' => '⁸', 250 '9' => '⁹', 251 _ => unreachable!(), 252 }); 253 } 254 out 255} 256 257#[cfg(test)] 258mod test { 259 use super::*; 260 #[test] 261 fn test_highlight() { 262 let input = "hello =world=\n"; 263 let output = parse(input).expect("parse to work"); 264 assert_eq!("hello \u{1b}[7mworld\u{1b}[0m\n", output.content); 265 } 266 267 #[test] 268 fn test_highlight_bad() { 269 let input = "hello =world\n"; 270 let output = parse(input).expect("parse to work"); 271 assert_eq!(input, output.content); 272 } 273 274 #[test] 275 fn test_link() { 276 let input = "hello [world](https://ngp.computer)\n"; 277 let output = parse(input).expect("parse to work"); 278 assert_eq!( 279 &[ParsedLink::External( 280 Url::parse("https://ngp.computer").unwrap() 281 )], 282 output.links.as_slice() 283 ); 284 assert_eq!( 285 "hello \u{1b}[34mworld\u{1b}[0m\u{1b}[35m¹\u{1b}[0m\n", 286 output.content 287 ); 288 } 289 290 #[test] 291 fn test_link_no_terminal_link() { 292 let input = "hello [world](https://ngp.computer\n"; 293 let output = parse(input).expect("parse to work"); 294 assert!(output.links.is_empty()); 295 assert_eq!(input, output.content); 296 } 297 #[test] 298 fn test_link_bad_no_start_link() { 299 let input = "hello [world]https://ngp.computer)\n"; 300 let output = parse(input).expect("parse to work"); 301 assert!(output.links.is_empty()); 302 assert_eq!(input, output.content); 303 } 304 #[test] 305 fn test_link_bad_no_link() { 306 let input = "hello [world]\n"; 307 let output = parse(input).expect("parse to work"); 308 assert!(output.links.is_empty()); 309 assert_eq!(input, output.content); 310 } 311 312 #[test] 313 fn test_internal_link_good() { 314 let input = "hello [[tsk-123]]\n"; 315 let output = parse(input).expect("parse to work"); 316 assert_eq!(&[ParsedLink::Internal(Id(123))], output.links.as_slice()); 317 assert_eq!( 318 "hello \u{1b}[35mtsk-123\u{1b}[0m\u{1b}[35m¹\u{1b}[0m\n", 319 output.content 320 ); 321 } 322 323 #[test] 324 fn test_internal_link_bad() { 325 let input = "hello [[tsk-123"; 326 let output = parse(input).expect("parse to work"); 327 assert!(output.links.is_empty()); 328 assert_eq!(input, output.content); 329 } 330 331 #[test] 332 fn test_italics() { 333 let input = "hello *world*\n"; 334 let output = parse(input).expect("parse to work"); 335 assert_eq!("hello \u{1b}[3mworld\u{1b}[0m\n", output.content); 336 } 337 338 #[test] 339 fn test_italics_bad() { 340 let input = "hello *world"; 341 let output = parse(input).expect("parse to work"); 342 assert_eq!(input, output.content); 343 } 344 345 #[test] 346 fn test_bold() { 347 let input = "hello !world!\n"; 348 let output = parse(input).expect("parse to work"); 349 assert_eq!("hello \u{1b}[1mworld\u{1b}[0m\n", output.content); 350 } 351 352 #[test] 353 fn test_bold_bad() { 354 let input = "hello !world\n"; 355 let output = parse(input).expect("parse to work"); 356 assert_eq!(input, output.content); 357 } 358 359 #[test] 360 fn test_underline() { 361 let input = "hello _world_\n"; 362 let output = parse(input).expect("parse to work"); 363 assert_eq!("hello \u{1b}[4mworld\u{1b}[0m\n", output.content); 364 } 365 366 #[test] 367 fn test_underline_bad() { 368 let input = "hello _world\n"; 369 let output = parse(input).expect("parse to work"); 370 assert_eq!(input, output.content); 371 } 372 373 #[test] 374 fn test_strikethrough() { 375 let input = "hello ~world~\n"; 376 let output = parse(input).expect("parse to work"); 377 assert_eq!("hello \u{1b}[9mworld\u{1b}[0m\n", output.content); 378 } 379 380 #[test] 381 fn test_strikethrough_bad() { 382 let input = "hello ~world\n"; 383 let output = parse(input).expect("parse to work"); 384 assert_eq!(input, output.content); 385 } 386 387 #[test] 388 fn test_inlineblock() { 389 let input = "hello `world`\n"; 390 let output = parse(input).expect("parse to work"); 391 assert_eq!("hello \u{1b}[32mworld\u{1b}[0m\n", output.content); 392 } 393 394 #[test] 395 fn test_inlineblock_bad() { 396 let input = "hello `world\n"; 397 let output = parse(input).expect("parse to work"); 398 assert_eq!(input, output.content); 399 } 400 401 #[test] 402 fn test_multiple_styles() { 403 let input = "hello *italic* ~strikethrough~ !bold!\n"; 404 let output = parse(input).expect("parse to work"); 405 assert_eq!( 406 "hello \u{1b}[3mitalic\u{1b}[0m \u{1b}[9mstrikethrough\u{1b}[0m \u{1b}[1mbold\u{1b}[0m\n", 407 output.content 408 ); 409 } 410}