1
0
Fork 0
mirror of https://github.com/archtechx/todo-system.git synced 2025-12-12 00:54:03 +00:00

todo.md parsing logic

This commit is contained in:
Samuel Štancl 2023-11-22 21:40:04 +01:00
parent 46d2ebacd3
commit c708a45ab9
4 changed files with 219 additions and 57 deletions

12
samples/todo.md Normal file
View file

@ -0,0 +1,12 @@
- generic foo
- [ ] generic bar
- todo00 priority bar
## High priority
- todo0 a
- foo
- [ ] bar
## Responsivity
- abc
- [ ] def

View file

@ -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<Entry> = 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);
}
}

View file

@ -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>) {
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<Entry>) {
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,28 +80,10 @@ pub fn scan_string(str: String, filename: PathBuf, entries: &mut Vec<Entry>) {
break;
}
// [0, 1, ..., 9]
let priority_chars: Vec<char> = (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::<isize>().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
}
let text_dirty = line.split_once(word).unwrap().1.replace("*/", "");
let text = text_dirty.trim();
if let Some(priority) = parse_priority(word) {
entries.push(Entry {
text: text.to_string(),
location: Location {
@ -100,6 +93,9 @@ pub fn scan_string(str: String, filename: PathBuf, entries: &mut Vec<Entry>) {
data: EntryData::Priority(priority),
});
}
break;
}
}
}
}
@ -141,6 +137,70 @@ pub fn scan_dir(path: &Path, entries: &mut Vec<Entry>, excludes: &Vec<PathBuf>,
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]
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<PathBuf> = 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<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]);
}

View file

@ -1,4 +1,3 @@
## CLI
- README.md
- todo.md