diff --git a/samples/todo.md b/samples/todo.md new file mode 100644 index 0000000..3642bfc --- /dev/null +++ b/samples/todo.md @@ -0,0 +1,12 @@ +- generic foo +- [ ] generic bar +- todo00 priority bar + +## High priority +- todo0 a +- foo +- [ ] bar + +## Responsivity +- abc +- [ ] def diff --git a/src/main.rs b/src/main.rs index 2e60730..af3d7fb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use clap::Parser; use crate::entries::Entry; use crate::render::render_entries; -use crate::scan::{Stats, scan_dir}; +use crate::scan::{Stats, scan_dir, scan_todo_file}; pub mod scan; pub mod render; @@ -13,11 +13,11 @@ pub mod entries; #[command(author, version, about, long_about = None)] struct Args { /// Path to your README.md file - #[arg(short, long, default_value = "")] + #[arg(short, long, default_value = "README.md")] readme: String, // Path to your todo.md file - #[arg(short, long, default_value = "")] + #[arg(short, long, default_value = "todo.md")] todos: String, // Paths to search @@ -56,17 +56,28 @@ fn main() { excludes.push(path); } - // todo@real logic for readme.md and todos.md + // todo@real logic for readme.md let mut entries: Vec = vec![]; let mut stats = Stats::new(); - scan_dir(root_dir.as_path(), &mut entries, &excludes, &mut stats).unwrap(); + for p in &paths { + scan_dir(p.as_path(), &mut entries, &excludes, &mut stats).unwrap(); + } + + let mut todos_path = root_dir.clone(); + todos_path.push(&args.todos); + + if todos_path.exists() { + scan_todo_file(&todos_path, &mut entries).unwrap(); + } render_entries(entries); if args.verbose { eprint!("\n\n"); stats.print(); + eprintln!("Paths: {:?}", &paths); + eprintln!("Excludes: {:?}", &excludes); } } diff --git a/src/scan.rs b/src/scan.rs index 8e4feb5..6759f8c 100644 --- a/src/scan.rs +++ b/src/scan.rs @@ -23,23 +23,36 @@ impl Stats { } } +fn parse_priority(word: &str) -> Option { + let lowercase_word = word.to_lowercase(); + let priority_substr = lowercase_word.split("todo").nth(1).unwrap(); + + if priority_substr.len() == 1 { + return Some(priority_substr.to_string().parse::().unwrap()); + } else if priority_substr.chars().all(|ch| ch == '0') { + // todo0: 1 - 1 = 0 + // todo00: 1 - 2 = -1 + return Some(1 - priority_substr.len() as isize); + } else { + return None; // invalid syntax like todo11 + } +} + pub fn scan_string(str: String, filename: PathBuf, entries: &mut Vec) { for (line_num, line) in str.lines().enumerate() { if ! line.to_lowercase().contains("todo") { continue; } - for word in line.split(" ") { + for word in line.split_whitespace() { if ! word.to_lowercase().starts_with("todo") { continue; } - // Handles: `todo`, `TODO`, `todo:`, `TODO:` - // todo@real `replace` isnt ideal, it should only replace *after* the todo, to avoid merging eg `to:do` - if word.to_lowercase().replace(':', "") == "todo" { - let text_dirty = line.split_once(word).unwrap().1.replace("*/", ""); - let text = text_dirty.trim(); + let text = line.split_once(word).unwrap().1.trim().trim_end_matches("*/").trim(); + // Handles: `todo`, `TODO`, `todo:`, `TODO:` + if word.to_lowercase().trim_end_matches(':') == "todo" { entries.push(Entry { text: text.to_string(), location: Location { @@ -54,8 +67,6 @@ pub fn scan_string(str: String, filename: PathBuf, entries: &mut Vec) { if word.contains('@') { let category = word.split('@').nth(1).unwrap(); - let text_dirty = line.split_once(word).unwrap().1.replace("*/", ""); - let text = text_dirty.trim(); entries.push(Entry { text: text.to_string(), @@ -69,36 +80,21 @@ pub fn scan_string(str: String, filename: PathBuf, entries: &mut Vec) { break; } - // [0, 1, ..., 9] - let priority_chars: Vec = (0..10).map(|int| char::from_digit(int, 10).unwrap()).collect(); + let priority_chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; if word.chars().any(|ch| priority_chars.contains(&ch)) { - let cleaned_word = word.to_lowercase(); - let priority_chars = cleaned_word.split("todo").nth(1).unwrap(); - - let priority: isize; - - if priority_chars.len() == 1 { - priority = priority_chars.to_string().parse::().unwrap(); - } else if priority_chars.chars().all(|ch| ch == '0') { - // todo0: 1 - 1 = 0 - // todo00: 1 - 2 = -1 - priority = 1 - priority_chars.len() as isize; - } else { - break; // incorrect syntax like todo11 + if let Some(priority) = parse_priority(word) { + entries.push(Entry { + text: text.to_string(), + location: Location { + file: filename.clone(), + line: line_num + 1, + }, + data: EntryData::Priority(priority), + }); } - let text_dirty = line.split_once(word).unwrap().1.replace("*/", ""); - let text = text_dirty.trim(); - - entries.push(Entry { - text: text.to_string(), - location: Location { - file: filename.clone(), - line: line_num + 1, - }, - data: EntryData::Priority(priority), - }); + break; } } } @@ -141,6 +137,70 @@ pub fn scan_dir(path: &Path, entries: &mut Vec, excludes: &Vec, Ok(()) } +pub fn scan_todo_file(path: &Path, entries: &mut Vec) -> io::Result<()> { + let str = fs::read_to_string(path)?; + let mut current_category: Option<&str> = None; + let priority_chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; + + // This can produce: + // - generic todos (above any category) + // - catgory todos (below a ## category heading) + // - priority todos (priority keyword part of the line) + 'line: for (line_num, line) in str.lines().enumerate() { + if line.starts_with('#') { + current_category = Some(line.split_once("# ").unwrap().1); + + continue; + } + + if ! line.starts_with('-') { + continue; + } + + for word in line.split_whitespace() { + if word.to_lowercase().starts_with("todo") && word.chars().any(|ch| priority_chars.contains(&ch)) { + if let Some(priority) = parse_priority(word) { + entries.push(Entry { + text: line.split_once(word).unwrap().1.trim().trim_end_matches("*/").trim().to_string(), + location: Location { + file: path.to_path_buf(), + line: line_num + 1, + }, + data: EntryData::Priority(priority), + }); + } + + continue 'line; + } + } + + let text = line.trim_start_matches("- [ ] ").trim_start_matches("- ").to_string(); + + if let Some(category) = current_category { + entries.push(Entry { + text, + location: Location { + file: path.to_path_buf(), + line: line_num + 1, + }, + data: EntryData::Category(category.to_string()), + }); + + continue; + } + + entries.push(Entry { + text, + location: Location { + file: path.to_path_buf(), + line: line_num + 1, + }, + data: EntryData::Generic, + }); + } + + Ok(()) +} #[test] fn generic_test() { @@ -403,14 +463,9 @@ fn sample_test_ts() { let mut path = std::env::current_dir().unwrap(); path.push("samples"); + path.push("1.ts"); - let mut filepath = path.clone(); - filepath.push("1.ts"); - - let excludes: Vec = vec![]; - let mut stats = Stats::new(); - - scan_dir(path.as_path(), &mut entries, &excludes, &mut stats).unwrap(); + scan_file(path.as_path(), &mut entries).unwrap(); assert_eq!(10, entries.len()); @@ -418,7 +473,7 @@ fn sample_test_ts() { data: EntryData::Category(String::from("types")), text: String::from(""), location: Location { - file: filepath.clone(), + file: path.clone(), line: 1, } }, entries[0]); @@ -427,7 +482,7 @@ fn sample_test_ts() { data: EntryData::Category(String::from("types")), text: String::from("add types"), location: Location { - file: filepath.clone(), + file: path.clone(), line: 5, } }, entries[1]); @@ -436,7 +491,7 @@ fn sample_test_ts() { data: EntryData::Priority(-2), text: String::from(""), location: Location { - file: filepath.clone(), + file: path.clone(), line: 10, } }, entries[2]); @@ -445,7 +500,7 @@ fn sample_test_ts() { data: EntryData::Priority(-1), text: String::from("add return typehint"), location: Location { - file: filepath.clone(), + file: path.clone(), line: 14, } }, entries[3]); @@ -454,7 +509,7 @@ fn sample_test_ts() { data: EntryData::Priority(0), text: String::from("add name typehint"), location: Location { - file: filepath.clone(), + file: path.clone(), line: 19, } }, entries[4]); @@ -463,7 +518,7 @@ fn sample_test_ts() { data: EntryData::Priority(1), text: String::from("add return typehint"), location: Location { - file: filepath.clone(), + file: path.clone(), line: 23, } }, entries[5]); @@ -472,7 +527,7 @@ fn sample_test_ts() { data: EntryData::Priority(2), text: String::from("add return typehint"), location: Location { - file: filepath.clone(), + file: path.clone(), line: 27, } }, entries[6]); @@ -481,7 +536,7 @@ fn sample_test_ts() { data: EntryData::Generic, text: String::from(""), location: Location { - file: filepath.clone(), + file: path.clone(), line: 31, } }, entries[7]); @@ -490,7 +545,7 @@ fn sample_test_ts() { data: EntryData::Generic, text: String::from("generic todo 2"), location: Location { - file: filepath.clone(), + file: path.clone(), line: 33, } }, entries[8]); @@ -499,8 +554,93 @@ fn sample_test_ts() { data: EntryData::Generic, text: String::from("generic todo 3"), location: Location { - file: filepath.clone(), + file: path.clone(), line: 34, } }, entries[9]); } + +#[test] +fn todo_file_test() { + let mut entries: Vec = vec![]; + + let mut path = std::env::current_dir().unwrap(); + path.push("samples"); + path.push("todo.md"); + + scan_todo_file(path.as_path(), &mut entries).unwrap(); + + assert_eq!(8, entries.len()); + + assert_eq!(Entry { + data: EntryData::Generic, + text: String::from("generic foo"), + location: Location { + file: path.clone(), + line: 1, + } + }, entries[0]); + + assert_eq!(Entry { + data: EntryData::Generic, + text: String::from("generic bar"), + location: Location { + file: path.clone(), + line: 2, + } + }, entries[1]); + + assert_eq!(Entry { + data: EntryData::Priority(-1), + text: String::from("priority bar"), + location: Location { + file: path.clone(), + line: 3, + } + }, entries[2]); + + assert_eq!(Entry { + data: EntryData::Priority(0), + text: String::from("a"), + location: Location { + file: path.clone(), + line: 6, + } + }, entries[3]); + + assert_eq!(Entry { + data: EntryData::Category(String::from("High priority")), + text: String::from("foo"), + location: Location { + file: path.clone(), + line: 7, + } + }, entries[4]); + + assert_eq!(Entry { + data: EntryData::Category(String::from("High priority")), + text: String::from("bar"), + location: Location { + file: path.clone(), + line: 8, + } + }, entries[5]); + + assert_eq!(Entry { + data: EntryData::Category(String::from("Responsivity")), + text: String::from("abc"), + location: Location { + file: path.clone(), + line: 11, + } + }, entries[6]); + + assert_eq!(Entry { + data: EntryData::Category(String::from("Responsivity")), + text: String::from("def"), + location: Location { + file: path.clone(), + line: 12, + } + }, entries[7]); +} diff --git a/todo.md b/todo.md index 8b01ef8..20b7984 100644 --- a/todo.md +++ b/todo.md @@ -1,4 +1,3 @@ ## CLI - README.md -- todo.md