vault_tasks_core/parser/
task.rs

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