From 01440710468281178ce1798086ef3ee79987dc12 Mon Sep 17 00:00:00 2001 From: Campbell Alden Date: Mon, 24 Jul 2023 11:02:14 +0900 Subject: [PATCH] Parse tag blocks out of project and task note sections --- src/main.rs | 21 +++++--- src/reporter.rs | 104 ++++++++++++++++++++++++++++-------- src/things/logbook.js | 2 +- src/things/logbook_cycle.js | 2 +- src/things/task.rs | 1 + src/things/today.js | 2 +- 6 files changed, 102 insertions(+), 30 deletions(-) diff --git a/src/main.rs b/src/main.rs index e5b2463..b72c3f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ mod things; mod reporter; mod emoji; -use reporter::{MarkdownReporter, Reporter, Resolution}; +use reporter::{MarkdownReporter, Reporter, Resolution, ReportOptions}; use things::task::{Task, Status}; use anyhow::Result; @@ -20,14 +20,20 @@ enum Modes { } impl Modes { - fn format_tasks(&self, tasks: Vec) -> String { + fn format_tasks(&self, tasks: Vec, tags: Vec) -> String { match self { Modes::Morning => { - let task_report = MarkdownReporter.report(tasks, &Resolution::FullTask); + let task_report = MarkdownReporter.report(tasks, &ReportOptions { + resolution: Resolution::FullTask, + tags, + }); format!("{}\n\n{}", emoji::pick(3).join(" "), task_report) }, Modes::Signoff => { - let task_report = MarkdownReporter.report(tasks, &Resolution::FullTask); + let task_report = MarkdownReporter.report(tasks, &ReportOptions { + resolution: Resolution::FullTask, + tags, + }); format!("Stopping now\n\n{}", task_report) }, Modes::Cycle => { @@ -37,7 +43,10 @@ impl Modes { } return false; }).collect::>(); - let task_report = MarkdownReporter.report(further_filtered, &Resolution::Project); + let task_report = MarkdownReporter.report(further_filtered, &ReportOptions { + resolution: Resolution::Project, + tags, + }); format!("*Cycle Report*\n\n{}", task_report) }, } @@ -73,7 +82,7 @@ fn main() -> Result<()> { let reported: Vec = tasks.into_iter().filter(|task| { args.tags.iter().all(|tag| task.has_tag(tag)) }).collect(); - let report = args.mode.format_tasks(reported); + let report = args.mode.format_tasks(reported, args.tags); println!("{report}"); Ok(()) diff --git a/src/reporter.rs b/src/reporter.rs index 258cdb2..0cc72f3 100644 --- a/src/reporter.rs +++ b/src/reporter.rs @@ -1,9 +1,41 @@ use crate::things::task::Task; +/// Given a notes field and a list of possible tags for sections, return the content of triple tick +/// blocks containing those tags +/// +/// # Examples +/// ``` +/// extract_tagged_notes( +/// "\`\`\`report +/// Something +/// \`\`\`", +/// vec![String::from("report")], +/// ); // -> "Something" +/// ``` +/// +fn extract_tagged_notes(notes: &str, tags: &Vec) -> Vec { + notes + .split("```") + .into_iter() + .map(|section| -> (&str, Option<&String>) { + (section, tags.iter().find(|t| section.starts_with(*t))) + }) + .filter(|(_section, tag)| { tag.is_some() }) + .map(|(section, tag)| { + let start_tag = tag.unwrap(); + section.strip_prefix(start_tag) + .map(|s| s.trim()) + .expect("Failed to strip start tag prefix").to_string() + }) + .collect() +} + + #[derive(Debug)] pub struct ProjectTree { id: String, title: String, + notes: Option, tasks: Vec, } @@ -39,6 +71,7 @@ impl AreaTree { self.projects.push(ProjectTree { id: project.id.clone(), title: project.title.clone(), + notes: project.notes.clone(), tasks: vec![task], }); } @@ -69,6 +102,7 @@ impl ThingsTree { self.hanging_projects.push(ProjectTree { id: project.id.clone(), title: project.title.clone(), + notes: project.notes.clone(), tasks: vec![task], }); } @@ -93,22 +127,27 @@ pub enum Resolution { Project, } +pub struct ReportOptions { + pub resolution: Resolution, + pub tags: Vec, +} + pub trait Reporter { - fn report_task(&mut self, task: &Task, depth: usize) -> String; - fn report_project(&mut self, project: &ProjectTree, depth: usize, resolution: &Resolution) -> String; - fn report_single_area(&mut self, area: &AreaTree, resolution: &Resolution) -> String; - fn report_multiple_areas(&mut self, areas: &Vec, resolution: &Resolution) -> String; - fn report(&mut self, tasks: Vec, resolution: &Resolution) -> String { + fn report_task(&mut self, task: &Task, depth: usize, options: &ReportOptions) -> String; + fn report_project(&mut self, project: &ProjectTree, depth: usize, options: &ReportOptions) -> String; + fn report_single_area(&mut self, area: &AreaTree, options: &ReportOptions) -> String; + fn report_multiple_areas(&mut self, areas: &Vec, options: &ReportOptions) -> String; + fn report(&mut self, tasks: Vec, options: &ReportOptions) -> String { let tree = ThingsTree::from_tasks(tasks); let untracked_tasks = tree.hanging_tasks .iter() - .map(|t| self.report_task(t, 0)) + .map(|t| self.report_task(t, 0, options)) .collect::>() .join("\n"); let area_tasks: String = match tree.areas.len() { 0 => "".to_string(), - 1 => self.report_single_area(&tree.areas[0], resolution), - _ => self.report_multiple_areas(&tree.areas, resolution), + 1 => self.report_single_area(&tree.areas[0], options), + _ => self.report_multiple_areas(&tree.areas, options), }; let separator = if area_tasks == "" || untracked_tasks == "" { @@ -124,44 +163,67 @@ pub trait Reporter { pub struct MarkdownReporter; impl Reporter for MarkdownReporter { - fn report_task(&mut self, task: &Task, depth: usize) -> String { - format!("{}- {}", String::from(" ").repeat(depth), task.title) + fn report_task(&mut self, task: &Task, depth: usize, options: &ReportOptions) -> String { + let relevant_notes = task.notes.clone() + .map(|notes| extract_tagged_notes(¬es, &options.tags)) + .unwrap_or(vec![]) + .iter() + .map(|l| format!("\n{}- {}", String::from(" ").repeat(depth + 4), l)) + .collect::>() + .join(""); + format!("\n{}- {}{}", String::from(" ").repeat(depth), task.title, relevant_notes) } - fn report_project(&mut self, project: &ProjectTree, depth: usize, resolution: &Resolution) -> String { + fn report_project(&mut self, project: &ProjectTree, depth: usize, options: &ReportOptions) -> String { + let resolution = &options.resolution; + let relevant_notes = project.notes.clone() + .map(|notes| extract_tagged_notes(¬es, &options.tags)) + .unwrap_or(vec![]) + .iter() + .map(|l| format!("\n{}- {}", String::from(" ").repeat(depth + 4), l)) + .collect::>() + .join(""); match resolution { Resolution::FullTask => { - let tasks = project.tasks.iter().map(|t| self.report_task(t, depth + 4)).collect::>().join("\n"); - format!("{}{}\n{}", String::from(" ").repeat(depth), project.title, tasks) + let tasks = project.tasks + .iter() + .map(|t| self.report_task(t, depth + 4, options)) + .collect::>() + .join(""); + format!("{}{}{}{}", String::from(" ").repeat(depth), relevant_notes, project.title, tasks) }, Resolution::Project => { - format!("{}- {}", String::from(" ").repeat(depth), project.title) + format!("{}- {}{}", String::from(" ").repeat(depth), project.title, relevant_notes) } } } - fn report_single_area(&mut self, area: &AreaTree, resolution: &Resolution) -> String { + fn report_single_area(&mut self, area: &AreaTree, options: &ReportOptions) -> String { let project_reports = area.projects .iter() - .map(|p| self.report_project(p, 0, resolution)) + .map(|p| self.report_project(p, 0, options)) .collect::>() .join("\n"); - let untracked_tasks = area.hanging_tasks.iter().map(|t| self.report_task(t, 0)).collect::>().join("\n"); + let untracked_tasks = area.hanging_tasks + .iter() + .map(|t| self.report_task(t, 0, options)) + .collect::>() + .join(""); let separator = if project_reports == "" || untracked_tasks == "" { "" } else { "\n\n" }; - match resolution { + match options.resolution { Resolution::FullTask => format!("{}{}{}", project_reports, separator, untracked_tasks), Resolution::Project => format!("{}", project_reports) } } - fn report_multiple_areas(&mut self, areas: &Vec, resolution: &Resolution) -> String { + fn report_multiple_areas(&mut self, areas: &Vec, options: &ReportOptions) -> String { match areas.len() { 0 => "".to_string(), - 1 => self.report_single_area(&areas[0], resolution), + 1 => self.report_single_area(&areas[0], options), _ => { areas.iter().map(|area| { - let single = self.report_single_area(area, resolution); + let single = self.report_single_area(area, options); format!("*{}*\n{}", area.title, single) }) .collect::>() diff --git a/src/things/logbook.js b/src/things/logbook.js index 52f5967..7124fcd 100644 --- a/src/things/logbook.js +++ b/src/things/logbook.js @@ -27,7 +27,7 @@ logbook.filter(task => { notes: todo.notes() || null, status: todo.status(), completion_date: todo.completionDate(), - project: proj && { id: proj.id(), title: proj.name(), status: proj.status() }, + project: proj && { id: proj.id(), title: proj.name(), status: proj.status(), notes: proj.notes() }, area: area && { id: area.id(), title: area.name() }, tags: [...tags, ...todo.tagNames().split(', ')].filter(t => t), }); diff --git a/src/things/logbook_cycle.js b/src/things/logbook_cycle.js index 368a256..b902324 100644 --- a/src/things/logbook_cycle.js +++ b/src/things/logbook_cycle.js @@ -27,7 +27,7 @@ logbook.filter(task => { notes: todo.notes() || null, status: todo.status(), completion_date: todo.completionDate(), - project: proj && { id: proj.id(), title: proj.name(), status: proj.status() }, + project: proj && { id: proj.id(), title: proj.name(), status: proj.status(), notes: proj.notes() }, area: area && { id: area.id(), title: area.name() }, tags: [...tags, ...todo.tagNames().split(', ')].filter(t => t), }); diff --git a/src/things/task.rs b/src/things/task.rs index c929adc..3e8e15b 100644 --- a/src/things/task.rs +++ b/src/things/task.rs @@ -19,6 +19,7 @@ pub enum Status { pub struct Project { pub id: String, pub title: String, + pub notes: Option, pub status: Status, } diff --git a/src/things/today.js b/src/things/today.js index ffcaa8d..8d57d48 100644 --- a/src/things/today.js +++ b/src/things/today.js @@ -15,7 +15,7 @@ today.forEach(todo => { notes: todo.notes() || null, status: todo.status(), completion_date: todo.completionDate(), - project: proj && { id: proj.id(), title: proj.name(), status: proj.status() }, + project: proj && { id: proj.id(), title: proj.name(), status: proj.status(), notes: proj.notes() }, area: area && { id: area.id(), title: area.name() }, tags: [...tags, ...todo.tagNames().split(', ')].filter(t => t), });