diff --git a/README.md b/README.md index 6471778..fba71ab 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,19 @@ Notes: - `node_modules/` (for npm) and `vendor/` (for composer) are ignored by default - paths starting with `.` are **always** ignored - `--exclude`s are relative to the current working directory, not passed paths (including default excludes mentioned above). If you're running the script for another folder and want to exclude folders there, type out the path in `--exclude` +- Passing any excludes overrides the default excludes, so if you want to *add* to the list of excludes, you need to re-define the default ones as well (e.g. `-e node_modules`) + +The tool also scans a todo.md file (path can be provided using `--todos`): +- all TODOs have to be list items (`- foo` or `- [ ] foo`) +- any TODOs *above* the first heading are considered generic TODOs +- any TODOs under a heading are considered category TODOs, with the heading being the category name +- any TODOs with numbers are added to the list of priority TODOs + +Scanning TODOs in a README.md file is also supported: +- all TODOs have to be list items (`- foo` or `- [ ] foo`) +- they have to be directly under a `TODO[s:]` (lower or uppercase) heading + +See the `samples/` folder for examples. To omit ANSI formatting and get raw markdown output, set `NO_COLOR=1` or `TERM=dumb`. diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 0000000..2b6fc26 --- /dev/null +++ b/samples/README.md @@ -0,0 +1,22 @@ +# Some project + +foo + +bar + +## Some section + +abc + +def + +- foo +- baz +- [ ] baz + +## TODOs + +- abc +- todo0 def +- [ ] bar +- [ ] baz diff --git a/src/main.rs b/src/main.rs index af3d7fb..23c9fa4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use clap::Parser; +use scan::scan_readme_file; use crate::entries::Entry; use crate::render::render_entries; use crate::scan::{Stats, scan_dir, scan_todo_file}; @@ -44,7 +45,12 @@ fn main() { for p in args.paths { let mut path = root_dir.clone(); - path.push(p); + + if p != "." { + // This isn't necessary and the code works just fine without it + // but it adds unnecessary /./ to the paths in the generated output. + path.push(p); + } paths.push(path); } @@ -56,8 +62,6 @@ fn main() { excludes.push(path); } - // todo@real logic for readme.md - let mut entries: Vec = vec![]; let mut stats = Stats::new(); @@ -72,6 +76,13 @@ fn main() { scan_todo_file(&todos_path, &mut entries).unwrap(); } + let mut readme_path = root_dir.clone(); + readme_path.push(&args.readme); + + if readme_path.exists() { + scan_readme_file(&readme_path, &mut entries).unwrap(); + } + render_entries(entries); if args.verbose { diff --git a/src/scan.rs b/src/scan.rs index 6759f8c..34fb088 100644 --- a/src/scan.rs +++ b/src/scan.rs @@ -2,6 +2,8 @@ use std::io; use std::fs; use std::path::{Path, PathBuf}; +const PRIORITY_CHARS: [char; 10] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; + use crate::entries::{Entry, EntryData, Location}; pub struct Stats { @@ -80,9 +82,7 @@ pub fn scan_string(str: String, filename: PathBuf, entries: &mut Vec) { break; } - let priority_chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; - - if word.chars().any(|ch| priority_chars.contains(&ch)) { + if word.chars().any(|ch| PRIORITY_CHARS.contains(&ch)) { if let Some(priority) = parse_priority(word) { entries.push(Entry { text: text.to_string(), @@ -140,7 +140,6 @@ pub fn scan_dir(path: &Path, entries: &mut Vec, excludes: &Vec, 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) @@ -158,7 +157,7 @@ pub fn scan_todo_file(path: &Path, entries: &mut Vec) -> io::Result<()> { } for word in line.split_whitespace() { - if word.to_lowercase().starts_with("todo") && word.chars().any(|ch| priority_chars.contains(&ch)) { + 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(), @@ -202,6 +201,65 @@ pub fn scan_todo_file(path: &Path, entries: &mut Vec) -> io::Result<()> { Ok(()) } +pub fn scan_readme_file(path: &Path, entries: &mut Vec) -> io::Result<()> { + let str = fs::read_to_string(path)?; + let mut in_todo_section = false; + + // 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('#') { + let section = line.split_once("# ").unwrap().1; + let cleaned_section = section.to_lowercase().trim_end_matches(':').trim().to_string(); + + if cleaned_section == "todo" || cleaned_section == "todos" { + in_todo_section = true; + } + + continue; + } + + if ! in_todo_section { + 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; + } + } + + // README.md can only have priority entries and generic entries + entries.push(Entry { + text: line.trim_start_matches("- [ ] ").trim_start_matches("- ").to_string(), + location: Location { + file: path.to_path_buf(), + line: line_num + 1, + }, + data: EntryData::Generic, + }); + } + + Ok(()) +} + #[test] fn generic_test() { let str = r#" @@ -644,3 +702,52 @@ fn todo_file_test() { } }, entries[7]); } + +#[test] +fn readme_file_test() { + let mut entries: Vec = vec![]; + + let mut path = std::env::current_dir().unwrap(); + path.push("samples"); + path.push("README.md"); + + scan_readme_file(path.as_path(), &mut entries).unwrap(); + + assert_eq!(4, entries.len()); + + assert_eq!(Entry { + data: EntryData::Generic, + text: String::from("abc"), + location: Location { + file: path.clone(), + line: 19, + } + }, entries[0]); + + assert_eq!(Entry { + data: EntryData::Priority(0), + text: String::from("def"), + location: Location { + file: path.clone(), + line: 20, + } + }, entries[1]); + + assert_eq!(Entry { + data: EntryData::Generic, + text: String::from("bar"), + location: Location { + file: path.clone(), + line: 21, + } + }, entries[2]); + + assert_eq!(Entry { + data: EntryData::Generic, + text: String::from("baz"), + location: Location { + file: path.clone(), + line: 22, + } + }, entries[3]); +} diff --git a/todo.md b/todo.md deleted file mode 100644 index 20b7984..0000000 --- a/todo.md +++ /dev/null @@ -1,3 +0,0 @@ -## CLI - -- README.md