use thiserror::Error;
#[derive(Debug, Error)]
pub enum WorkNoteError {
#[error(
"malformed task item at line {line}: {content:?} \
(expected \"- [ ] <text>\" or \"- [x] <text>\")"
)]
MalformedItem { line: usize, content: String },
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WorkItem {
pub complete: bool,
pub text: String,
pub line: usize,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct WorkChecklist {
pub title: Option<String>,
pub complete: bool,
pub items: Vec<WorkItem>,
}
impl WorkChecklist {
pub fn progress(&self) -> (usize, usize) {
let complete = self.items.iter().filter(|item| item.complete).count();
(complete, self.items.len())
}
}
pub fn parse_work_note(content: &str) -> Result<WorkChecklist, WorkNoteError> {
let mut checklist = WorkChecklist::default();
for (index, line) in content.lines().enumerate() {
let number = index + 1;
if checklist.title.is_none()
&& let Some((complete, title)) = title_line(line)
{
checklist.title = Some(title.trim().to_string());
checklist.complete = complete;
continue;
}
let stripped = line.trim_start();
if let Some((complete, text)) = item_line(stripped) {
checklist.items.push(WorkItem {
complete,
text: text.trim().to_string(),
line: number,
});
} else if stripped.starts_with("- [") {
return Err(WorkNoteError::MalformedItem {
line: number,
content: line.trim_end().to_string(),
});
}
}
Ok(checklist)
}
fn title_line(line: &str) -> Option<(bool, &str)> {
let rest = line.strip_prefix("# ")?;
checkbox(rest)
}
fn item_line(stripped: &str) -> Option<(bool, &str)> {
let rest = stripped.strip_prefix("- ")?;
checkbox(rest)
}
fn checkbox(rest: &str) -> Option<(bool, &str)> {
for (marker, complete) in [("[ ]", false), ("[x]", true)] {
if let Some(after) = rest.strip_prefix(marker)
&& (after.is_empty() || after.starts_with(' '))
{
return Some((complete, after));
}
}
None
}