1use std::iter::Peekable;
2
3use color_eyre::{eyre::bail, Result};
4use tracing::{debug, error};
5use winnow::{
6 ascii::{space0, space1},
7 combinator::{alt, preceded, repeat},
8 token::{take_till, take_while},
9 PResult, Parser,
10};
11
12use crate::{task::Task, vault_data::VaultData, TasksConfig};
13
14use super::task::parse_task;
15
16enum FileToken {
17 Header((String, usize)),
19 Description(String, usize),
21 Task(Task, usize),
23 FileTag(String),
25}
26
27#[allow(clippy::module_name_repetitions)]
28pub struct ParserFileEntry<'a> {
29 pub config: &'a TasksConfig,
30 pub filename: String,
31}
32
33impl ParserFileEntry<'_> {
34 fn parse_indent(input: &mut &str) -> PResult<usize> {
35 let indent_length: String = repeat(1.., " ").parse_next(input)?;
36 Ok(indent_length.len())
37 }
38 fn parse_task(&self, input: &mut &str) -> PResult<FileToken> {
39 let indent_length = Self::parse_indent(input).unwrap_or(0);
40
41 let mut task_parser =
42 |input: &mut &str| parse_task(input, self.filename.clone(), self.config);
43 let task_res = task_parser.parse_next(input)?;
44 Ok(FileToken::Task(task_res, indent_length))
45 }
46 fn parse_header(input: &mut &str) -> PResult<FileToken> {
47 let header_depth: String = repeat(1.., "#").parse_next(input)?;
48 let header_content = preceded(space0, take_till(1.., |c| c == '\n')).parse_next(input)?;
49
50 Ok(FileToken::Header((
51 header_content.to_string(),
52 header_depth.len(),
53 )))
54 }
55 fn parse_description(input: &mut &str) -> PResult<FileToken> {
56 let indent_length = space1.map(|s: &str| s.len()).parse_next(input)?;
57 let desc_content = take_till(1.., |c| c == '\n').parse_next(input)?;
58 Ok(FileToken::Description(
59 desc_content.to_string(),
60 indent_length,
61 ))
62 }
63 fn parse_file_tag(input: &mut &str) -> PResult<FileToken> {
64 let tag = preceded(
65 '#',
66 take_while(1.., ('_', '0'..='9', 'A'..='Z', 'a'..='z', '0'..='9')),
67 )
68 .parse_next(input)?;
69 Ok(FileToken::FileTag(tag.to_owned()))
70 }
71 fn insert_task_at(
72 file_entry: &mut VaultData,
73 task: Task,
74 header_depth: usize,
75 indent_length: usize,
76 ) -> Result<()> {
77 fn append_task_aux(
78 file_entry: &mut VaultData,
79 task_to_insert: Task,
80 current_header_depth: usize,
81 target_header_depth: usize,
82 current_task_depth: usize,
83 target_task_depth: usize,
84 ) -> Result<()> {
85 match file_entry {
86 VaultData::Header(_, _, header_children) => {
87 match current_header_depth.cmp(&target_header_depth) {
88 std::cmp::Ordering::Greater => panic!("Target header level was greater than current level which is impossible"), std::cmp::Ordering::Equal => {
90 if current_task_depth == target_task_depth {
92 header_children.push(VaultData::Task(task_to_insert));
93 Ok(())
94 } else {
95 for child in header_children.iter_mut().rev() {
96 if let VaultData::Task(_task) = child {
97 return append_task_aux(
98 child,
99 task_to_insert,
100 current_header_depth,
101 target_header_depth,
102 current_task_depth + 1,
103 target_task_depth,
104 );
105 }
106 }
107 bail!("Cound't find correct parent task to insert task {}", task_to_insert.name)
108 }
109 }
110 std::cmp::Ordering::Less => {
111 for child in header_children.iter_mut().rev() {
113 if let VaultData::Header(_,_, _) = child {
114 return append_task_aux(
115 child,
116 task_to_insert,
117 current_header_depth + 1,
118 target_header_depth,
119 current_task_depth,
120 target_task_depth,
121 );
122 }
123 }
124 bail!("Cound't find correct parent header to insert task {}", task_to_insert.name)
125 }
126 }
127 }
128 VaultData::Task(task) => {
129 let mut current_task_depth = current_task_depth;
130 let mut last_task = task;
131 while current_task_depth < target_task_depth {
132 if last_task.subtasks.is_empty() {
133 error!("Could not find parent task, indenting may be wrong. Closest task line number: {}",last_task.line_number);
134 bail!("Failed to insert task")
135 }
136 last_task = last_task.subtasks.last_mut().unwrap();
137 current_task_depth += 1;
138 }
139 last_task.subtasks.push(task_to_insert);
140 Ok(())
141 }
142 VaultData::Directory(_, _) => {
143 bail!("Failed to insert task: tried to insert into a directory")
144 }
145 }
146 }
147 append_task_aux(file_entry, task, 0, header_depth, 0, indent_length)
148 }
149 fn insert_header_at(
151 file_entry: &mut VaultData,
152 object: VaultData,
153 target_header_depth: usize,
154 target_task_depth: usize,
155 ) {
156 fn insert_at_aux(
157 file_entry: &mut VaultData,
158 object: VaultData,
159 current_header_depth: usize,
160 target_header_depth: usize,
161 current_task_depth: usize,
162 target_task_depth: usize,
163 ) {
164 match file_entry {
165 VaultData::Header(_, _, header_children) => {
166 match (current_header_depth).cmp(&target_header_depth) {
167 std::cmp::Ordering::Greater => error!(
168 "bad call to `insert_at`, file_entry:{file_entry}\nobject:{object}"
169 ), std::cmp::Ordering::Equal => {
171 if current_task_depth == target_task_depth {
173 header_children.push(object);
174 } else {
175 for child in header_children.iter_mut().rev() {
176 if let VaultData::Task(_) = child {
177 return insert_at_aux(
178 child,
179 object,
180 current_header_depth,
181 target_header_depth,
182 current_task_depth + 1,
183 target_task_depth,
184 );
185 }
186 }
187 }
188 }
189 std::cmp::Ordering::Less => {
190 for child in header_children.iter_mut().rev() {
192 if let VaultData::Header(_, _, _) = child {
193 insert_at_aux(
194 child,
195 object,
196 current_header_depth + 1,
197 target_header_depth,
198 current_task_depth,
199 target_task_depth,
200 );
201 return;
202 }
203 }
204 header_children.push(object);
205 }
206 }
207 }
208 VaultData::Task(_) => {
209 error!("Error: tried to insert a header into a task");
210 }
211 VaultData::Directory(name, _) => {
212 error!("Error: tried to insert a header into a directory : {name}");
213 }
214 }
215 }
216 insert_at_aux(
217 file_entry,
218 object,
219 0,
220 target_header_depth,
221 0,
222 target_task_depth,
223 );
224 }
225
226 fn append_description(
228 file_entry: &mut VaultData,
229 desc: String,
230 target_header_depth: usize,
231 target_task_depth: usize,
232 ) -> Result<()> {
233 fn append_description_aux(
234 file_entry: &mut VaultData,
235 desc: String,
236 current_header_depth: usize,
237 target_header_depth: usize,
238 current_task_depth: usize,
239 target_task_depth: usize,
240 ) -> Result<()> {
241 match file_entry {
242 VaultData::Header(_, _, header_children) => {
243 match current_header_depth.cmp(&target_header_depth) {
244 std::cmp::Ordering::Greater => panic!("bad call for desc"), std::cmp::Ordering::Equal => {
246 for child in header_children.iter_mut().rev() {
248 if let VaultData::Task(mut task) = child.clone() {
249 if current_task_depth == target_task_depth {
250 match &mut task.description {
251 Some(d) => {
252 d.push('\n');
253 d.push_str(&desc.clone());
254 }
255 None => task.description = Some(desc.clone()),
256 }
257 *child = VaultData::Task(task);
258 } else {
259 return append_description_aux(
260 child,
261 desc,
262 current_header_depth,
263 target_header_depth,
264 current_task_depth + 1,
265 target_task_depth,
266 );
267 }
268 }
269 }
270 Ok(())
271 }
272 std::cmp::Ordering::Less => {
273 for child in header_children.iter_mut().rev() {
275 if let VaultData::Header(_, _, _) = child {
276 return append_description_aux(
277 child,
278 desc,
279 current_header_depth + 1,
280 target_header_depth,
281 current_task_depth,
282 target_task_depth,
283 );
284 }
285 }
286 bail!("Failed to insert description: previous task not found");
287 }
288 }
289 }
290 VaultData::Task(task) => {
291 fn insert_desc_task(
292 description: String,
293 task: &mut Task,
294 current_level: usize,
295 target_level: usize,
296 ) -> Result<()> {
297 if current_level == target_level {
298 match &mut task.description {
299 Some(d) => {
300 d.push('\n');
301 d.push_str(&description);
302 Ok(())
303 }
304 None => {
305 task.description = Some(description.clone());
306 Ok(())
307 }
308 }
309 } else if let Some(task) = task.subtasks.last_mut() {
310 insert_desc_task(description, task, current_level + 1, target_level)
311 } else {
312 debug!("Description was too indented, adding to closest task: {description}");
313 insert_desc_task(description, task, current_level + 1, target_level)
314 }
315 }
316 insert_desc_task(desc, task, current_task_depth, target_task_depth)
317 }
318 VaultData::Directory(_, _) => {
319 bail!("Failed to insert description: tried to insert into a directory")
320 }
321 }
322 }
323 append_description_aux(
324 file_entry,
325 desc,
326 0,
327 target_header_depth,
328 0,
329 target_task_depth,
330 )
331 }
332
333 fn parse_file_aux<'a, I>(
335 &self,
336 mut input: Peekable<I>,
337 file_entry: &mut VaultData,
338 file_tags: &mut Vec<String>,
339 header_depth: usize,
340 ) where
341 I: Iterator<Item = (usize, &'a str)>,
342 {
343 let mut parser = alt((
344 Self::parse_file_tag,
345 Self::parse_header,
346 |input: &mut &str| self.parse_task(input),
347 Self::parse_description,
348 ));
349
350 let line_opt = input.next();
351 if line_opt.is_none() {
352 return;
353 }
354
355 let (line_number, mut line) = line_opt.unwrap();
356
357 match parser.parse_next(&mut line) {
358 Ok(FileToken::Task(mut task, indent_length)) => {
359 task.line_number = line_number + 1; if Self::insert_task_at(
361 file_entry,
362 task,
363 header_depth,
364 indent_length / self.config.indent_length,
365 )
366 .is_err()
367 {
368 error!("Failed to insert task");
369 }
370 self.parse_file_aux(input, file_entry, file_tags, header_depth);
371 }
372 Ok(FileToken::Header((header, new_depth))) => {
373 Self::insert_header_at(
374 file_entry,
375 VaultData::Header(new_depth, header, vec![]),
376 new_depth - 1,
377 0,
378 );
379 self.parse_file_aux(input, file_entry, file_tags, new_depth);
380 }
381 Ok(FileToken::Description(description, indent_length)) => {
382 if Self::append_description(
383 file_entry,
384 description.clone(),
385 header_depth,
386 indent_length / self.config.indent_length,
387 )
388 .is_err()
389 {
390 error!("Failed to insert description {description}");
391 }
392 self.parse_file_aux(input, file_entry, file_tags, header_depth);
393 }
394 Ok(FileToken::FileTag(tag)) => {
395 if !file_tags.contains(&tag) {
396 file_tags.push(tag);
397 }
398 self.parse_file_aux(input, file_entry, file_tags, header_depth);
399 }
400 Err(_) => self.parse_file_aux(input, file_entry, file_tags, header_depth),
401 }
402 }
403
404 fn clean_file_entry(file_entry: &mut VaultData) -> Option<&VaultData> {
406 match file_entry {
407 VaultData::Header(_, _, children) | VaultData::Directory(_, children) => {
408 let mut actual_children = vec![];
409 for child in children.iter_mut() {
410 let mut child_clone = child.clone();
411 if Self::clean_file_entry(&mut child_clone).is_some() {
412 actual_children.push(child_clone);
413 }
414 }
415 *children = actual_children;
416 if children.is_empty() {
417 None
418 } else {
419 Some(file_entry)
420 }
421 }
422 VaultData::Task(_) => Some(file_entry),
423 }
424 }
425
426 pub fn parse_file(&mut self, filename: &str, input: &&str) -> Option<VaultData> {
427 let lines = input.split('\n');
428
429 let mut res = VaultData::Header(0, filename.to_owned(), vec![]);
430 let mut file_tags = vec![];
431 self.filename = filename.to_string();
432 self.parse_file_aux(lines.enumerate().peekable(), &mut res, &mut file_tags, 0);
433
434 if self.config.file_tags_propagation {
435 file_tags.iter().for_each(|t| add_global_tag(&mut res, t));
436 }
437
438 if let Some(VaultData::Header(_, name, children)) = Self::clean_file_entry(&mut res) {
440 Some(VaultData::Directory(name.clone(), children.clone()))
441 } else {
442 None
443 }
444 }
445}
446
447fn add_global_tag(file_entry: &mut VaultData, tag: &String) {
448 fn add_tag_aux(file_entry: &mut VaultData, tag: &String) {
449 match file_entry {
450 VaultData::Header(_, _, children) | VaultData::Directory(_, children) => {
451 for child in children.iter_mut().rev() {
452 add_tag_aux(child, tag);
453 }
454 }
455 VaultData::Task(task) => {
456 fn insert_tag_task(task: &mut Task, tag: &String) {
457 match task.tags.clone() {
458 Some(mut tags) if !tags.contains(tag) => {
459 tags.push(tag.to_string());
460 task.tags = Some(tags);
461 }
462 None => task.tags = Some(vec![tag.to_string()]),
463 _ => (),
464 }
465
466 for st in &mut task.subtasks {
467 insert_tag_task(st, tag);
468 }
469 }
470 insert_tag_task(task, tag);
471 }
472 }
473 }
474 add_tag_aux(file_entry, tag);
475}
476#[cfg(test)]
477mod tests {
478
479 use insta::assert_snapshot;
480
481 use super::ParserFileEntry;
482
483 use crate::{
484 parser::parser_file_entry::add_global_tag, task::Task, vault_data::VaultData, TasksConfig,
485 };
486 #[test]
487 fn test_with_useless_headers() {
488 let input = r"# 1 useless
489## 2 useless
490### 3 useless
491
492# 2 useful
493### 3 useless
494## 4 useful
495- [ ] test
496 test
497 desc
498"
499 .split('\n')
500 .enumerate()
501 .peekable();
502
503 let config = TasksConfig {
504 indent_length: 2,
505 ..Default::default()
506 };
507 let mut res = VaultData::Header(0, "Test".to_string(), vec![]);
508 let parser = ParserFileEntry {
509 config: &config,
510 filename: String::new(),
511 };
512 let expected = VaultData::Header(
513 0,
514 "Test".to_string(),
515 vec![
516 VaultData::Header(
517 1,
518 "1 useless".to_string(),
519 vec![VaultData::Header(
520 2,
521 "2 useless".to_string(),
522 vec![VaultData::Header(3, "3 useless".to_string(), vec![])],
523 )],
524 ),
525 VaultData::Header(
526 1,
527 "2 useful".to_string(),
528 vec![
529 VaultData::Header(3, "3 useless".to_string(), vec![]),
530 VaultData::Header(
531 2,
532 "4 useful".to_string(),
533 vec![VaultData::Task(Task {
534 name: "test".to_string(),
535 line_number: 8,
536 description: Some("test\ndesc".to_string()),
537 ..Default::default()
538 })],
539 ),
540 ],
541 ),
542 ],
543 );
544 parser.parse_file_aux(input, &mut res, &mut vec![], 0);
545 assert_eq!(res, expected);
546
547 let expected_after_cleaning = VaultData::Header(
548 0,
549 "Test".to_string(),
550 vec![VaultData::Header(
551 1,
552 "2 useful".to_string(),
553 vec![VaultData::Header(
554 2,
555 "4 useful".to_string(),
556 vec![VaultData::Task(Task {
557 name: "test".to_string(),
558 line_number: 8,
559 description: Some("test\ndesc".to_string()),
560 ..Default::default()
561 })],
562 )],
563 )],
564 );
565 ParserFileEntry::clean_file_entry(&mut res);
566 assert_eq!(res, expected_after_cleaning);
567 }
568 #[test]
569 fn test_simple_input() {
570 let input = r"# 1 Header
571- [ ] Task
572
573## 2 Header
574### 3 Header
575- [ ] Task
576- [ ] Task 2
577## 2 Header 2
578- [ ] Task
579 Description
580
581"
582 .split('\n')
583 .enumerate()
584 .peekable();
585
586 let config = TasksConfig {
587 indent_length: 2,
588 ..Default::default()
589 };
590 let mut res = VaultData::Header(0, "Test".to_string(), vec![]);
591 let parser = ParserFileEntry {
592 config: &config,
593 filename: String::new(),
594 };
595 let expected = VaultData::Header(
596 0,
597 "Test".to_string(),
598 vec![VaultData::Header(
599 1,
600 "1 Header".to_string(),
601 vec![
602 VaultData::Task(Task {
603 name: "Task".to_string(),
604 line_number: 2,
605 ..Default::default()
606 }),
607 VaultData::Header(
608 2,
609 "2 Header".to_string(),
610 vec![VaultData::Header(
611 3,
612 "3 Header".to_string(),
613 vec![
614 VaultData::Task(Task {
615 name: "Task".to_string(),
616 line_number: 6,
617 ..Default::default()
618 }),
619 VaultData::Task(Task {
620 name: "Task 2".to_string(),
621 line_number: 7,
622 ..Default::default()
623 }),
624 ],
625 )],
626 ),
627 VaultData::Header(
628 2,
629 "2 Header 2".to_string(),
630 vec![VaultData::Task(Task {
631 name: "Task".to_string(),
632 line_number: 9,
633 description: Some("Description".to_string()),
634 ..Default::default()
635 })],
636 ),
637 ],
638 )],
639 );
640 parser.parse_file_aux(input, &mut res, &mut vec![], 0);
641 assert_eq!(res, expected);
642 }
643 #[test]
644 fn test_insert_global_tag() {
645 let input = r"# 1 Header
646- [ ] Task
647
648## 2 Header
649### 3 Header
650- [ ] Task
651- [ ] Task 2
652## 2 Header 2
653- [ ] Task
654 Description
655
656"
657 .split('\n')
658 .enumerate()
659 .peekable();
660
661 let config = TasksConfig {
662 indent_length: 2,
663 ..Default::default()
664 };
665 let mut res = VaultData::Header(0, "Test".to_string(), vec![]);
666 let parser = ParserFileEntry {
667 config: &config,
668 filename: String::new(),
669 };
670 parser.parse_file_aux(input, &mut res, &mut vec![], 0);
671 add_global_tag(&mut res, &String::from("test"));
672 assert_snapshot!(res);
673 }
674 #[test]
675 fn test_fake_description() {
676 let input = r"# 1 Header
677 test
678- [ ] Task
679
680## 2 Header
681 test
682"
683 .split('\n')
684 .enumerate()
685 .peekable();
686
687 let config = TasksConfig {
688 indent_length: 2,
689 ..Default::default()
690 };
691 let mut res = VaultData::Header(0, "Test".to_string(), vec![]);
692 let parser = ParserFileEntry {
693 config: &config,
694 filename: String::new(),
695 };
696 let expected = VaultData::Header(
697 0,
698 "Test".to_string(),
699 vec![VaultData::Header(
700 1,
701 "1 Header".to_string(),
702 vec![
703 VaultData::Task(Task {
704 name: "Task".to_string(),
705 line_number: 3,
706 ..Default::default()
707 }),
708 VaultData::Header(2, "2 Header".to_string(), vec![]),
709 ],
710 )],
711 );
712 parser.parse_file_aux(input, &mut res, &mut vec![], 0);
713 assert_eq!(res, expected);
714 }
715 #[test]
716 fn test_nested_tasks() {
717 let input = r"# 1 Header
718## Test
719- [ ] Test a
720 - [ ] Test b
721 - [ ] Test c
722"
723 .split('\n')
724 .enumerate()
725 .peekable();
726
727 let config = TasksConfig {
728 indent_length: 2,
729 ..Default::default()
730 };
731 let mut res = VaultData::Header(0, "Test".to_string(), vec![]);
732 let parser = ParserFileEntry {
733 config: &config,
734 filename: String::new(),
735 };
736 let expected = VaultData::Header(
737 0,
738 "Test".to_string(),
739 vec![VaultData::Header(
740 1,
741 "1 Header".to_string(),
742 vec![VaultData::Header(
743 2,
744 "Test".to_string(),
745 vec![VaultData::Task(Task {
746 name: "Test a".to_string(),
747 line_number: 3,
748 subtasks: vec![Task {
749 name: "Test b".to_string(),
750 line_number: 4,
751 subtasks: vec![Task {
752 name: "Test c".to_string(),
753 line_number: 5,
754 ..Default::default()
755 }],
756 ..Default::default()
757 }],
758 ..Default::default()
759 })],
760 )],
761 )],
762 );
763 parser.parse_file_aux(input, &mut res, &mut vec![], 0);
764 println!("{res:#?}");
765 assert_eq!(res, expected);
766 }
767 #[test]
768 fn test_nested_tasks_desc() {
769 let input = r"# 1 Header
770- [ ] t1
771 t1
772 - [ ] t2
773 t2
774 t1
775 t2
776 - [ ] t3
777 t2
778 t1
779 t3
780 t1
781 - [ ] t4
782 t2
783 t4
784 t3
785 t4
786
787"
788 .split('\n')
789 .enumerate()
790 .peekable();
791
792 let config = TasksConfig {
793 indent_length: 2,
794 ..Default::default()
795 };
796 let mut res = VaultData::Header(0, "Test".to_string(), vec![]);
797 let parser = ParserFileEntry {
798 config: &config,
799 filename: String::new(),
800 };
801 parser.parse_file_aux(input, &mut res, &mut vec![], 0);
802 assert_snapshot!(res);
803 }
804}