mirror of
https://github.com/archtechx/todo-system.git
synced 2025-12-12 09:04:03 +00:00
scan todos in readme.md
This commit is contained in:
parent
c708a45ab9
commit
5737232baa
5 changed files with 161 additions and 11 deletions
13
README.md
13
README.md
|
|
@ -136,6 +136,19 @@ Notes:
|
||||||
- `node_modules/` (for npm) and `vendor/` (for composer) are ignored by default
|
- `node_modules/` (for npm) and `vendor/` (for composer) are ignored by default
|
||||||
- paths starting with `.` are **always** ignored
|
- 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`
|
- `--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`.
|
To omit ANSI formatting and get raw markdown output, set `NO_COLOR=1` or `TERM=dumb`.
|
||||||
|
|
||||||
|
|
|
||||||
22
samples/README.md
Normal file
22
samples/README.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Some project
|
||||||
|
|
||||||
|
foo
|
||||||
|
|
||||||
|
bar
|
||||||
|
|
||||||
|
## Some section
|
||||||
|
|
||||||
|
abc
|
||||||
|
|
||||||
|
def
|
||||||
|
|
||||||
|
- foo
|
||||||
|
- baz
|
||||||
|
- [ ] baz
|
||||||
|
|
||||||
|
## TODOs
|
||||||
|
|
||||||
|
- abc
|
||||||
|
- todo0 def
|
||||||
|
- [ ] bar
|
||||||
|
- [ ] baz
|
||||||
15
src/main.rs
15
src/main.rs
|
|
@ -1,6 +1,7 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use scan::scan_readme_file;
|
||||||
use crate::entries::Entry;
|
use crate::entries::Entry;
|
||||||
use crate::render::render_entries;
|
use crate::render::render_entries;
|
||||||
use crate::scan::{Stats, scan_dir, scan_todo_file};
|
use crate::scan::{Stats, scan_dir, scan_todo_file};
|
||||||
|
|
@ -44,7 +45,12 @@ fn main() {
|
||||||
|
|
||||||
for p in args.paths {
|
for p in args.paths {
|
||||||
let mut path = root_dir.clone();
|
let mut path = root_dir.clone();
|
||||||
|
|
||||||
|
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);
|
path.push(p);
|
||||||
|
}
|
||||||
|
|
||||||
paths.push(path);
|
paths.push(path);
|
||||||
}
|
}
|
||||||
|
|
@ -56,8 +62,6 @@ fn main() {
|
||||||
excludes.push(path);
|
excludes.push(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo@real logic for readme.md
|
|
||||||
|
|
||||||
let mut entries: Vec<Entry> = vec![];
|
let mut entries: Vec<Entry> = vec![];
|
||||||
let mut stats = Stats::new();
|
let mut stats = Stats::new();
|
||||||
|
|
||||||
|
|
@ -72,6 +76,13 @@ fn main() {
|
||||||
scan_todo_file(&todos_path, &mut entries).unwrap();
|
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);
|
render_entries(entries);
|
||||||
|
|
||||||
if args.verbose {
|
if args.verbose {
|
||||||
|
|
|
||||||
117
src/scan.rs
117
src/scan.rs
|
|
@ -2,6 +2,8 @@ use std::io;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
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};
|
use crate::entries::{Entry, EntryData, Location};
|
||||||
|
|
||||||
pub struct Stats {
|
pub struct Stats {
|
||||||
|
|
@ -80,9 +82,7 @@ pub fn scan_string(str: String, filename: PathBuf, entries: &mut Vec<Entry>) {
|
||||||
break;
|
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) {
|
if let Some(priority) = parse_priority(word) {
|
||||||
entries.push(Entry {
|
entries.push(Entry {
|
||||||
text: text.to_string(),
|
text: text.to_string(),
|
||||||
|
|
@ -140,7 +140,6 @@ pub fn scan_dir(path: &Path, entries: &mut Vec<Entry>, excludes: &Vec<PathBuf>,
|
||||||
pub fn scan_todo_file(path: &Path, entries: &mut Vec<Entry>) -> io::Result<()> {
|
pub fn scan_todo_file(path: &Path, entries: &mut Vec<Entry>) -> io::Result<()> {
|
||||||
let str = fs::read_to_string(path)?;
|
let str = fs::read_to_string(path)?;
|
||||||
let mut current_category: Option<&str> = None;
|
let mut current_category: Option<&str> = None;
|
||||||
let priority_chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
|
||||||
|
|
||||||
// This can produce:
|
// This can produce:
|
||||||
// - generic todos (above any category)
|
// - generic todos (above any category)
|
||||||
|
|
@ -158,7 +157,7 @@ pub fn scan_todo_file(path: &Path, entries: &mut Vec<Entry>) -> io::Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
for word in line.split_whitespace() {
|
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) {
|
if let Some(priority) = parse_priority(word) {
|
||||||
entries.push(Entry {
|
entries.push(Entry {
|
||||||
text: line.split_once(word).unwrap().1.trim().trim_end_matches("*/").trim().to_string(),
|
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<Entry>) -> io::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scan_readme_file(path: &Path, entries: &mut Vec<Entry>) -> 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]
|
#[test]
|
||||||
fn generic_test() {
|
fn generic_test() {
|
||||||
let str = r#"
|
let str = r#"
|
||||||
|
|
@ -644,3 +702,52 @@ fn todo_file_test() {
|
||||||
}
|
}
|
||||||
}, entries[7]);
|
}, entries[7]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn readme_file_test() {
|
||||||
|
let mut entries: Vec<Entry> = 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]);
|
||||||
|
}
|
||||||
|
|
|
||||||
3
todo.md
3
todo.md
|
|
@ -1,3 +0,0 @@
|
||||||
## CLI
|
|
||||||
|
|
||||||
- README.md
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue