1mod parse_completion;
2mod parse_today;
3mod parser_priorities;
4mod parser_state;
5mod parser_tags;
6mod token;
7
8use std::path::Path;
9
10use chrono::NaiveDateTime;
11use parse_completion::parse_completion;
12use parse_today::parse_today;
13use parser_priorities::parse_priority;
14use parser_state::parse_task_state;
15use parser_tags::parse_tag;
16use token::Token;
17use tracing::error;
18use winnow::{
19 Parser, Result,
20 combinator::{alt, fail, repeat},
21 token::any,
22};
23
24use crate::{TasksConfig, date::Date, task::Task};
25
26use super::{
27 parser_date::{self},
28 parser_time,
29};
30
31fn parse_token(input: &mut &str, config: &TasksConfig) -> Result<Token> {
33 alt((
34 |input: &mut &str| {
36 let date = parser_date::parse_naive_date(input, config.core.use_american_format)?;
37 Ok(Token::DueDate(date))
38 },
39 |input: &mut &str| {
40 let date = parser_time::parse_naive_time(input)?;
41 Ok(Token::DueTime(date))
42 },
43 parse_tag,
44 |input: &mut &str| parse_task_state(input, &config.task_state_markers),
45 parse_priority,
46 parse_completion,
47 parse_today,
48 |input: &mut &str| {
49 let res = repeat(0.., any)
50 .fold(String::new, |mut string, c| {
51 string.push(c);
52 string
53 })
54 .parse_next(input)?;
55 Ok(Token::Name(res))
56 },
57 ))
58 .parse_next(input)
59}
60
61#[allow(clippy::module_name_repetitions)]
67pub fn parse_task(input: &mut &str, path: &Path, config: &TasksConfig) -> Result<Task> {
68 let task_state = match parse_task_state(input, &config.task_state_markers)? {
69 Token::State(state) => Ok(state),
70 _ => fail(input),
71 }?;
72
73 let mut token_parser = |input: &mut &str| parse_token(input, config);
74
75 let tokens = input
76 .split_ascii_whitespace()
77 .map(|token| token_parser.parse(token));
78
79 let mut task = Task {
80 state: task_state,
81 path: path.to_path_buf(),
82 ..Default::default()
83 };
84
85 let mut due_date_opt = None;
87 let mut due_time_opt = None;
88 let mut name_vec = vec![]; for token_res in tokens {
91 match token_res {
92 Ok(Token::DueDate(date)) => due_date_opt = Some(date),
93 Ok(Token::DueTime(time)) => due_time_opt = Some(time),
94 Ok(Token::Name(name)) => name_vec.push(name),
95 Ok(Token::Priority(p)) => task.priority = p,
96 Ok(Token::State(state)) => task.state = state,
97 Ok(Token::Tag(tag)) => {
98 if let Some(ref mut tags) = task.tags {
99 tags.push(tag);
100 } else {
101 task.tags = Some(vec![tag]);
102 }
103 }
104 Ok(Token::TodayFlag) => task.is_today = true,
105 Ok(Token::CompletionPercentage(c)) => task.completion = Some(c),
106 Err(error) => error!("Error: {error:?}"),
107 }
108 }
109
110 if !name_vec.is_empty() {
111 task.name = name_vec.join(" ");
112 }
113 let due_date = if let Some(due_date) = due_date_opt {
114 match due_time_opt {
115 Some(time) => Some(Date::DayTime(NaiveDateTime::new(due_date, time))),
116 None => Some(Date::Day(due_date)),
117 }
118 } else {
119 None
120 };
121
122 task.due_date = due_date;
123 Ok(task)
124}
125#[cfg(test)]
126mod test {
127
128 use std::path::PathBuf;
129
130 use chrono::{Datelike, Days, NaiveDate, NaiveDateTime, NaiveTime};
131
132 use crate::{
133 TasksConfig,
134 date::Date,
135 parser::task::parse_task,
136 task::{State, Task},
137 };
138 #[test]
139 fn test_parse_task_no_description() {
140 let mut input = "- [x] 10/15 task_name #done";
141 let config = TasksConfig {
142 ..Default::default()
143 };
144 let res = parse_task(&mut input, PathBuf::new().as_path(), &config);
145 assert!(res.is_ok());
146 let res = res.unwrap();
147 let year = chrono::Local::now().year();
148 let expected = Task {
149 name: "task_name".to_string(),
150 description: None,
151 tags: Some(vec!["done".to_string()]),
152 due_date: Some(Date::Day(NaiveDate::from_ymd_opt(year, 10, 15).unwrap())),
153 priority: 0,
154 state: State::Done,
155 line_number: None,
156 ..Default::default()
157 };
158 assert_eq!(res, expected);
159 }
160
161 #[test]
162 fn test_parse_task_only_state() {
163 let mut input = "- [ ]";
164 let config = TasksConfig::default();
165 let res = parse_task(&mut input, PathBuf::new().as_path(), &config);
166 assert!(res.is_ok());
167 let res = res.unwrap();
168 let expected = Task {
169 subtasks: vec![],
170 name: String::new(),
171 description: None,
172 tags: None,
173 due_date: None,
174 priority: 0,
175 state: State::ToDo,
176 line_number: None,
177 path: PathBuf::new(),
178 is_today: false,
179 completion: None,
180 };
181 assert_eq!(res, expected);
182 }
183 #[test]
184 fn test_parse_task_with_due_date_words() {
185 let mut input = "- [ ] today 15:30 task_name";
186 let config = TasksConfig::default();
187 let res = parse_task(&mut input, PathBuf::new().as_path(), &config);
188 assert!(res.is_ok());
189 let res = res.unwrap();
190 let expected_date = chrono::Local::now().date_naive();
191 let expected_time = NaiveTime::from_hms_opt(15, 30, 0).unwrap();
192 let expected_due_date = Date::DayTime(NaiveDateTime::new(expected_date, expected_time));
193 assert_eq!(res.due_date, Some(expected_due_date));
194 }
195
196 #[test]
197 fn test_parse_task_with_weekday() {
198 let mut input = "- [ ] monday 15:30 task_name";
199 let config = TasksConfig::default();
200 let res = parse_task(&mut input, PathBuf::new().as_path(), &config);
201 assert!(res.is_ok());
202 let res = res.unwrap();
203
204 let now = chrono::Local::now();
205 let expected_date = now
206 .date_naive()
207 .checked_add_days(Days::new(
208 8 - u64::from(now.date_naive().weekday().number_from_monday()),
209 ))
210 .unwrap();
211 let expected_time = NaiveTime::from_hms_opt(15, 30, 0).unwrap();
212 let expected_due_date = Date::DayTime(NaiveDateTime::new(expected_date, expected_time));
213 assert_eq!(res.due_date, Some(expected_due_date));
214 }
215
216 #[test]
217 fn test_parse_task_with_weekday_this() {
218 let mut input = "- [ ] this monday 15:30 task_name";
219 let config = TasksConfig::default();
220 let res = parse_task(&mut input, PathBuf::new().as_path(), &config);
221 assert!(res.is_ok());
222 let res = res.unwrap();
223 let now = chrono::Local::now();
224 let expected_date = now
225 .date_naive()
226 .checked_add_days(Days::new(
227 8 - u64::from(now.date_naive().weekday().number_from_monday()),
228 ))
229 .unwrap();
230 let expected_time = NaiveTime::from_hms_opt(15, 30, 0).unwrap();
231 let expected_due_date = Date::DayTime(NaiveDateTime::new(expected_date, expected_time));
232 assert_eq!(res.due_date, Some(expected_due_date));
233 }
234
235 #[test]
236 fn test_parse_task_with_weekday_next() {
237 let mut input = "- [ ] next monday 15:30 task_name";
238 let config = TasksConfig::default();
239 let res = parse_task(&mut input, PathBuf::new().as_path(), &config);
240 assert!(res.is_ok());
241 let res = res.unwrap();
242 let now = chrono::Local::now();
243 let expected_date = now
244 .date_naive()
245 .checked_add_days(Days::new(
246 8 - u64::from(now.date_naive().weekday().number_from_monday()),
247 ))
248 .unwrap();
249 let expected_time = NaiveTime::from_hms_opt(15, 30, 0).unwrap();
250 let expected_due_date = Date::DayTime(NaiveDateTime::new(expected_date, expected_time));
251 assert_eq!(res.due_date, Some(expected_due_date));
252 }
253
254 #[test]
255 fn test_parse_task_without_due_date() {
256 let mut input = "- [ ] task_name";
257 let config = TasksConfig::default();
258 let res = parse_task(&mut input, PathBuf::new().as_path(), &config);
259 assert!(res.is_ok());
260 let res = res.unwrap();
261 let expected_due_date = None;
262 assert_eq!(res.due_date, expected_due_date);
263 }
264
265 #[test]
266 fn test_parse_task_with_invalid_state() {
267 let mut input = "- [invalid] task_name";
268 let config = TasksConfig::default();
269 let res = parse_task(&mut input, PathBuf::new().as_path(), &config);
270 assert!(res.is_err());
271 }
272
273 #[test]
274 fn test_parse_task_without_state() {
275 let mut input = "task_name";
276 let config = TasksConfig::default();
277 let res = parse_task(&mut input, PathBuf::new().as_path(), &config);
278 assert!(res.is_err());
279 }
280
281 #[test]
282 fn test_parse_task_with_invalid_priority() {
283 let mut input = "- [ ] task_name p-9";
284 let config = TasksConfig::default();
285 let res = parse_task(&mut input, PathBuf::new().as_path(), &config);
286 assert!(res.is_ok());
287 let res = res.unwrap();
288 assert_eq!(res.priority, 0);
289 }
290
291 #[test]
292 fn test_parse_task_without_name() {
293 let mut input = "- [ ]";
294 let config = TasksConfig::default();
295 let res = parse_task(&mut input, PathBuf::new().as_path(), &config);
296 assert!(res.is_ok());
297 let res = res.unwrap();
298 assert_eq!(res.name, ""); }
300 #[test]
301 fn test_parse_task_with_today_flag() {
302 let mut input = "- [ ] @t";
303 let config = TasksConfig::default();
304 let res = parse_task(&mut input, PathBuf::new().as_path(), &config);
305 assert!(res.is_ok());
306 let res = res.unwrap();
307 assert!(res.is_today);
308 }
309}