Skip to main content

vault_tasks_core/parser/
task.rs

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
31/// Parses a `Token` from an input string.FileEntry
32fn parse_token(input: &mut &str, config: &TasksConfig) -> Result<Token> {
33    alt((
34        // |input: &mut &str| (parse_naive_date(input, config.use_american_format)),
35        |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/// Parses a `Task` from an input string. Filename must be specified to be added to the task.
62///
63/// # Errors
64///
65/// Will return an error if the task can't be parsed.
66#[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    // Placeholders for a date and a time
86    let mut due_date_opt = None;
87    let mut due_time_opt = None;
88    let mut name_vec = vec![]; // collects words that aren't tokens from the input string
89
90    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, ""); // Default name is used when no name is provided
299    }
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}