mirror of
https://github.com/archtechx/todo-system.git
synced 2025-12-12 09:04:03 +00:00
todo.md parsing logic
This commit is contained in:
parent
46d2ebacd3
commit
c708a45ab9
4 changed files with 219 additions and 57 deletions
12
samples/todo.md
Normal file
12
samples/todo.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
- generic foo
|
||||||
|
- [ ] generic bar
|
||||||
|
- todo00 priority bar
|
||||||
|
|
||||||
|
## High priority
|
||||||
|
- todo0 a
|
||||||
|
- foo
|
||||||
|
- [ ] bar
|
||||||
|
|
||||||
|
## Responsivity
|
||||||
|
- abc
|
||||||
|
- [ ] def
|
||||||
21
src/main.rs
21
src/main.rs
|
|
@ -3,7 +3,7 @@ use std::path::PathBuf;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use crate::entries::Entry;
|
use crate::entries::Entry;
|
||||||
use crate::render::render_entries;
|
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 scan;
|
||||||
pub mod render;
|
pub mod render;
|
||||||
|
|
@ -13,11 +13,11 @@ pub mod entries;
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
struct Args {
|
struct Args {
|
||||||
/// Path to your README.md file
|
/// Path to your README.md file
|
||||||
#[arg(short, long, default_value = "")]
|
#[arg(short, long, default_value = "README.md")]
|
||||||
readme: String,
|
readme: String,
|
||||||
|
|
||||||
// Path to your todo.md file
|
// Path to your todo.md file
|
||||||
#[arg(short, long, default_value = "")]
|
#[arg(short, long, default_value = "todo.md")]
|
||||||
todos: String,
|
todos: String,
|
||||||
|
|
||||||
// Paths to search
|
// Paths to search
|
||||||
|
|
@ -56,17 +56,28 @@ fn main() {
|
||||||
excludes.push(path);
|
excludes.push(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo@real logic for readme.md and todos.md
|
// 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();
|
||||||
|
|
||||||
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);
|
render_entries(entries);
|
||||||
|
|
||||||
if args.verbose {
|
if args.verbose {
|
||||||
eprint!("\n\n");
|
eprint!("\n\n");
|
||||||
stats.print();
|
stats.print();
|
||||||
|
eprintln!("Paths: {:?}", &paths);
|
||||||
|
eprintln!("Excludes: {:?}", &excludes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
242
src/scan.rs
242
src/scan.rs
|
|
@ -23,23 +23,36 @@ impl Stats {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_priority(word: &str) -> Option<isize> {
|
||||||
|
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::<isize>().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<Entry>) {
|
pub fn scan_string(str: String, filename: PathBuf, entries: &mut Vec<Entry>) {
|
||||||
for (line_num, line) in str.lines().enumerate() {
|
for (line_num, line) in str.lines().enumerate() {
|
||||||
if ! line.to_lowercase().contains("todo") {
|
if ! line.to_lowercase().contains("todo") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for word in line.split(" ") {
|
for word in line.split_whitespace() {
|
||||||
if ! word.to_lowercase().starts_with("todo") {
|
if ! word.to_lowercase().starts_with("todo") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles: `todo`, `TODO`, `todo:`, `TODO:`
|
let text = line.split_once(word).unwrap().1.trim().trim_end_matches("*/").trim();
|
||||||
// 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();
|
|
||||||
|
|
||||||
|
// Handles: `todo`, `TODO`, `todo:`, `TODO:`
|
||||||
|
if word.to_lowercase().trim_end_matches(':') == "todo" {
|
||||||
entries.push(Entry {
|
entries.push(Entry {
|
||||||
text: text.to_string(),
|
text: text.to_string(),
|
||||||
location: Location {
|
location: Location {
|
||||||
|
|
@ -54,8 +67,6 @@ pub fn scan_string(str: String, filename: PathBuf, entries: &mut Vec<Entry>) {
|
||||||
|
|
||||||
if word.contains('@') {
|
if word.contains('@') {
|
||||||
let category = word.split('@').nth(1).unwrap();
|
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 {
|
entries.push(Entry {
|
||||||
text: text.to_string(),
|
text: text.to_string(),
|
||||||
|
|
@ -69,36 +80,21 @@ pub fn scan_string(str: String, filename: PathBuf, entries: &mut Vec<Entry>) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// [0, 1, ..., 9]
|
let priority_chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||||
let priority_chars: Vec<char> = (0..10).map(|int| char::from_digit(int, 10).unwrap()).collect();
|
|
||||||
|
|
||||||
if word.chars().any(|ch| priority_chars.contains(&ch)) {
|
if word.chars().any(|ch| priority_chars.contains(&ch)) {
|
||||||
let cleaned_word = word.to_lowercase();
|
if let Some(priority) = parse_priority(word) {
|
||||||
let priority_chars = cleaned_word.split("todo").nth(1).unwrap();
|
entries.push(Entry {
|
||||||
|
text: text.to_string(),
|
||||||
let priority: isize;
|
location: Location {
|
||||||
|
file: filename.clone(),
|
||||||
if priority_chars.len() == 1 {
|
line: line_num + 1,
|
||||||
priority = priority_chars.to_string().parse::<isize>().unwrap();
|
},
|
||||||
} else if priority_chars.chars().all(|ch| ch == '0') {
|
data: EntryData::Priority(priority),
|
||||||
// todo0: 1 - 1 = 0
|
});
|
||||||
// todo00: 1 - 2 = -1
|
|
||||||
priority = 1 - priority_chars.len() as isize;
|
|
||||||
} else {
|
|
||||||
break; // incorrect syntax like todo11
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let text_dirty = line.split_once(word).unwrap().1.replace("*/", "");
|
break;
|
||||||
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),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -141,6 +137,70 @@ pub fn scan_dir(path: &Path, entries: &mut Vec<Entry>, excludes: &Vec<PathBuf>,
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scan_todo_file(path: &Path, entries: &mut Vec<Entry>) -> 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]
|
#[test]
|
||||||
fn generic_test() {
|
fn generic_test() {
|
||||||
|
|
@ -403,14 +463,9 @@ fn sample_test_ts() {
|
||||||
|
|
||||||
let mut path = std::env::current_dir().unwrap();
|
let mut path = std::env::current_dir().unwrap();
|
||||||
path.push("samples");
|
path.push("samples");
|
||||||
|
path.push("1.ts");
|
||||||
|
|
||||||
let mut filepath = path.clone();
|
scan_file(path.as_path(), &mut entries).unwrap();
|
||||||
filepath.push("1.ts");
|
|
||||||
|
|
||||||
let excludes: Vec<PathBuf> = vec![];
|
|
||||||
let mut stats = Stats::new();
|
|
||||||
|
|
||||||
scan_dir(path.as_path(), &mut entries, &excludes, &mut stats).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(10, entries.len());
|
assert_eq!(10, entries.len());
|
||||||
|
|
||||||
|
|
@ -418,7 +473,7 @@ fn sample_test_ts() {
|
||||||
data: EntryData::Category(String::from("types")),
|
data: EntryData::Category(String::from("types")),
|
||||||
text: String::from(""),
|
text: String::from(""),
|
||||||
location: Location {
|
location: Location {
|
||||||
file: filepath.clone(),
|
file: path.clone(),
|
||||||
line: 1,
|
line: 1,
|
||||||
}
|
}
|
||||||
}, entries[0]);
|
}, entries[0]);
|
||||||
|
|
@ -427,7 +482,7 @@ fn sample_test_ts() {
|
||||||
data: EntryData::Category(String::from("types")),
|
data: EntryData::Category(String::from("types")),
|
||||||
text: String::from("add types"),
|
text: String::from("add types"),
|
||||||
location: Location {
|
location: Location {
|
||||||
file: filepath.clone(),
|
file: path.clone(),
|
||||||
line: 5,
|
line: 5,
|
||||||
}
|
}
|
||||||
}, entries[1]);
|
}, entries[1]);
|
||||||
|
|
@ -436,7 +491,7 @@ fn sample_test_ts() {
|
||||||
data: EntryData::Priority(-2),
|
data: EntryData::Priority(-2),
|
||||||
text: String::from(""),
|
text: String::from(""),
|
||||||
location: Location {
|
location: Location {
|
||||||
file: filepath.clone(),
|
file: path.clone(),
|
||||||
line: 10,
|
line: 10,
|
||||||
}
|
}
|
||||||
}, entries[2]);
|
}, entries[2]);
|
||||||
|
|
@ -445,7 +500,7 @@ fn sample_test_ts() {
|
||||||
data: EntryData::Priority(-1),
|
data: EntryData::Priority(-1),
|
||||||
text: String::from("add return typehint"),
|
text: String::from("add return typehint"),
|
||||||
location: Location {
|
location: Location {
|
||||||
file: filepath.clone(),
|
file: path.clone(),
|
||||||
line: 14,
|
line: 14,
|
||||||
}
|
}
|
||||||
}, entries[3]);
|
}, entries[3]);
|
||||||
|
|
@ -454,7 +509,7 @@ fn sample_test_ts() {
|
||||||
data: EntryData::Priority(0),
|
data: EntryData::Priority(0),
|
||||||
text: String::from("add name typehint"),
|
text: String::from("add name typehint"),
|
||||||
location: Location {
|
location: Location {
|
||||||
file: filepath.clone(),
|
file: path.clone(),
|
||||||
line: 19,
|
line: 19,
|
||||||
}
|
}
|
||||||
}, entries[4]);
|
}, entries[4]);
|
||||||
|
|
@ -463,7 +518,7 @@ fn sample_test_ts() {
|
||||||
data: EntryData::Priority(1),
|
data: EntryData::Priority(1),
|
||||||
text: String::from("add return typehint"),
|
text: String::from("add return typehint"),
|
||||||
location: Location {
|
location: Location {
|
||||||
file: filepath.clone(),
|
file: path.clone(),
|
||||||
line: 23,
|
line: 23,
|
||||||
}
|
}
|
||||||
}, entries[5]);
|
}, entries[5]);
|
||||||
|
|
@ -472,7 +527,7 @@ fn sample_test_ts() {
|
||||||
data: EntryData::Priority(2),
|
data: EntryData::Priority(2),
|
||||||
text: String::from("add return typehint"),
|
text: String::from("add return typehint"),
|
||||||
location: Location {
|
location: Location {
|
||||||
file: filepath.clone(),
|
file: path.clone(),
|
||||||
line: 27,
|
line: 27,
|
||||||
}
|
}
|
||||||
}, entries[6]);
|
}, entries[6]);
|
||||||
|
|
@ -481,7 +536,7 @@ fn sample_test_ts() {
|
||||||
data: EntryData::Generic,
|
data: EntryData::Generic,
|
||||||
text: String::from(""),
|
text: String::from(""),
|
||||||
location: Location {
|
location: Location {
|
||||||
file: filepath.clone(),
|
file: path.clone(),
|
||||||
line: 31,
|
line: 31,
|
||||||
}
|
}
|
||||||
}, entries[7]);
|
}, entries[7]);
|
||||||
|
|
@ -490,7 +545,7 @@ fn sample_test_ts() {
|
||||||
data: EntryData::Generic,
|
data: EntryData::Generic,
|
||||||
text: String::from("generic todo 2"),
|
text: String::from("generic todo 2"),
|
||||||
location: Location {
|
location: Location {
|
||||||
file: filepath.clone(),
|
file: path.clone(),
|
||||||
line: 33,
|
line: 33,
|
||||||
}
|
}
|
||||||
}, entries[8]);
|
}, entries[8]);
|
||||||
|
|
@ -499,8 +554,93 @@ fn sample_test_ts() {
|
||||||
data: EntryData::Generic,
|
data: EntryData::Generic,
|
||||||
text: String::from("generic todo 3"),
|
text: String::from("generic todo 3"),
|
||||||
location: Location {
|
location: Location {
|
||||||
file: filepath.clone(),
|
file: path.clone(),
|
||||||
line: 34,
|
line: 34,
|
||||||
}
|
}
|
||||||
}, entries[9]);
|
}, entries[9]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn todo_file_test() {
|
||||||
|
let mut entries: Vec<Entry> = 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]);
|
||||||
|
}
|
||||||
|
|
|
||||||
1
todo.md
1
todo.md
|
|
@ -1,4 +1,3 @@
|
||||||
## CLI
|
## CLI
|
||||||
|
|
||||||
- README.md
|
- README.md
|
||||||
- todo.md
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue